CMS for React Native
ElmapiCMS is a self-hosted headless CMS with a REST API and official JavaScript SDK. This guide shows the production-safe mobile pattern: keep CMS credentials on your backend, expose app-ready endpoints, cache in React Native, and use webhooks to trigger content sync workflows.
A good fit for mobile apps
Mobile teams often need to ship copy, onboarding content, support pages, and campaign blocks without waiting for app store review. ElmapiCMS provides that dynamic content layer through a predictable API.
Because it is self-hosted and project-scoped, you can run one CMS instance for multiple apps and environments while keeping access control in your backend.
If you are also shipping a web frontend, pair this guide with CMS for Next.js so both channels share one content backend.
Use a secure API gateway pattern
For React Native, the recommended flow is App -> Your API -> ElmapiCMS. This keeps privileged tokens off devices and lets you shape responses for mobile performance.
- Backend owns credentials: store
ELMAPI_API_KEYonly in server env. - App calls your endpoints: use app auth, rate limiting, and response shaping.
- CMS remains the source of truth: editors publish once and all clients consume the same content.
Install SDK and configure environment variables
Install the official SDK in your backend service:
npm install @elmapicms/js-sdkUse separate environment variables for backend and app runtime:
# Backend service env
ELMAPI_API_URL=https://your-cms.example.com/api
ELMAPI_PROJECT_ID=your-project-uuid
ELMAPI_API_KEY=your-server-token
ELMAPI_WEBHOOK_SECRET=your-webhook-secret
# React Native app env
EXPO_PUBLIC_API_BASE_URL=https://api.yourapp.comGenerate Project IDs and API tokens from Settings → API Access. For mobile delivery, use backend-scoped tokens and never embed them in app binaries.
Create a shared CMS client and app-facing routes
Start with one backend utility that initializes the SDK. Keep all error handling and configuration in this layer.
// backend/lib/elmapi.ts
import { createClient } from "@elmapicms/js-sdk";
export function cms() {
const url = process.env.ELMAPI_API_URL;
const key = process.env.ELMAPI_API_KEY;
const projectId = process.env.ELMAPI_PROJECT_ID;
if (!url || !key || !projectId) {
throw new Error("Missing ElmapiCMS environment variables");
}
return createClient(url, key, projectId);
}Mobile feed endpoint
Query only the fields needed by the app and return a lean payload for fast rendering.
// backend/routes/mobile-feed.ts (Express example)
import { Router } from "express";
import { cms } from "../lib/elmapi";
const router = Router();
router.get("/mobile/feed", async (_req, res, next) => {
try {
const result = await cms().getEntries("mobile_feed", {
state: "published",
sort: "created_at:desc",
paginate: 20,
page: 1,
fields: ["title", "summary", "hero_image", "cta_url", "updated_at"],
});
const data = Array.isArray(result) ? result : result.data;
res.json({ items: data });
} catch (error) {
next(error);
}
});
export default router;Consume content with React Query
Use React Query to cache network responses and reduce repeated calls while users navigate between screens.
// app/src/features/feed/useFeed.ts
import { useQuery } from "@tanstack/react-query";
const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL;
async function fetchFeed() {
const response = await fetch(`${API_BASE_URL}/mobile/feed`);
if (!response.ok) {
throw new Error("Failed to fetch feed");
}
return response.json();
}
export function useFeed() {
return useQuery({
queryKey: ["mobile-feed"],
queryFn: fetchFeed,
staleTime: 60_000,
});
}Slug detail endpoint
Return 404 for missing content so app state can handle unavailable routes cleanly.
// backend/routes/mobile-post.ts
router.get("/mobile/posts/:slug", async (req, res, next) => {
try {
const result = await cms().getEntries("posts", {
where: { slug: req.params.slug },
limit: 1,
state: "published",
});
const rows = Array.isArray(result) ? result : result.data;
const post = rows[0];
if (!post) return res.status(404).json({ error: "Not found" });
return res.json({ item: post });
} catch (error) {
next(error);
}
});Use webhooks for cache and refresh flows
ElmapiCMS webhooks can notify your backend when content changes. From there you can purge edge caches, warm mobile endpoints, or trigger in-app refresh signals.
// backend/routes/elmapi-webhook.ts
import { createHmac, timingSafeEqual } from "node:crypto";
router.post("/webhooks/elmapicms", express.text({ type: "*/*" }), async (req, res) => {
const rawBody = req.body ?? "";
const signature = req.header("x-webhook-signature");
const secret = process.env.ELMAPI_WEBHOOK_SECRET;
if (secret) {
if (!signature) return res.status(401).json({ error: "Missing signature" });
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const valid =
signature.length === expected.length &&
timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!valid) return res.status(401).json({ error: "Invalid signature" });
}
// Optional: purge edge cache or notify app clients via push/in-app refresh.
return res.json({ ok: true });
});Configure webhook events in Project Settings → Webhooksand review production queue requirements in the deployment webhooks docs.
Before you ship
- No privileged CMS tokens are embedded in React Native app code.
- Backend endpoints fetch only
state: "published"content for public clients. - List endpoints are paginated and only request fields used by the screen.
- App caching strategy is defined (React Query stale times and retry behavior).
- Webhook signature verification is enabled before cache invalidation logic.