Using a Headless CMS as a Backend for Mobile Apps
Ship articles, banners, and onboarding copy without waiting on app-store review
Every mobile team eventually hits the same wall. A marketing person wants to change one promo banner, and the honest answer is: "Sure — it'll be live in next week's release, assuming review goes through on time." Content compiled into the app binary moves at app-store speed. Content served from an API moves at publish speed.
TL;DR: A headless CMS works well as a mobile app backend — content editors publish to an API, the app fetches it at runtime. The key decisions are what to cache locally, how to handle images, and how to keep auth secrets out of the binary.
That's the whole case for using a headless CMS as a mobile app backend. Articles, FAQs, onboarding screens, promo banners, help docs — anything an editor might want to change — gets fetched over a REST API at runtime instead of baked into the build. This guide covers the architecture, what to fetch and when, offline handling, image sizing, auth, webhook-driven cache busting, and the cases where a CMS is the wrong tool entirely.
What Belongs in a CMS (and Why Not Hardcode It)
A headless CMS works for mobile apps when the content is editorial: written by a few people, read by many, changed on no fixed schedule. The usual suspects:
- In-app articles, news, and blog content
- FAQ and help documentation
- Onboarding screens — copy, images, screen order
- Promo banners and seasonal campaigns
- Legal pages (terms, privacy) that legal teams revise without warning
Teams usually handle this content one of two ways before reaching for a CMS, and both are worse.
Hardcoding. Every copy tweak becomes a build, a review cycle, a release, and then days of waiting for users to actually update. Worse: users on old versions see old content forever. A pricing FAQ that's wrong in version 2.3 stays wrong on every phone still running 2.3.
Building a custom admin panel. This starts as "a quick internal tool" and ends as a bad CMS. You're rebuilding auth, roles, a text editor, media uploads, and publishing workflow — weeks of engineering for a worse version of software that already exists. If you're comparing options for this job, our rundown of self-hosted Strapi alternatives covers the field.
With a CMS in place, an editor changes the banner, hits publish, and the app shows it on the next fetch. No deploy. No release train. No engineer in the loop.
The Architecture: App → API → CMS
Keep it boring. Three layers:
┌────────────┐ ┌──────────────┐ ┌──────────────┐
│ Mobile app │ ───▶ │ Cache / CDN │ ───▶ │ Headless CMS │
│ (iOS/And.) │ HTTPS│ (edge cache) │ │ (REST API) │
└────────────┘ └──────────────┘ └──────────────┘
The app only ever reads published content through public API endpoints. It never touches the admin interface, and it never holds credentials (more on that below).
The cache layer in the middle is not optional at scale. If 100,000 users open your app at 9 a.m., that should not be 100,000 database queries against your CMS. Put a CDN in front of the API, or run a thin backend proxy with a Redis cache. The CMS becomes the origin that only sees cache misses.
UnfoldCMS, for example, exposes everything under /api/v1/* — posts, pages, categories, search, menus, and settings are all public read endpoints:
curl "https://cms.example.com/api/v1/posts?per_page=20"
{
"data": [
{
"title": "How to reset your password",
"slug": "how-to-reset-your-password",
"excerpt": "Step-by-step instructions...",
"posted_at": "2026-06-10T09:00:00Z"
}
],
"meta": { "current_page": 1, "per_page": 20, "total": 84 }
}
One nice side effect: this is the same API your web properties consume. The pattern in our Nuxt integration guide — fetch JSON, render it — is identical on mobile, just with URLSession or Retrofit instead of $fetch.
What to Fetch, and When
Don't fetch everything at launch. Match the request to the moment the user needs the content:
| Content | Typical endpoint | When to fetch | Cache for |
|---|---|---|---|
| App-launch config | /api/v1/settings |
On cold start | 5–15 min |
| Content lists | /api/v1/posts?category=faq |
When the screen opens | 15–60 min |
| Detail views | /api/v1/posts/{slug} |
On tap | Hours, revalidate |
| Menus / navigation | /api/v1/menus/{key} |
On cold start | Hours |
App-launch config is the highest-impact one. One small settings payload fetched at startup can drive the promo banner (on/off, text, deep link), the support email, maintenance-mode messaging, and which onboarding variant to show. Editors flip a value in the CMS; every app launch after that reflects it.
Lists should load when the user opens the relevant screen, not preemptively at launch. The FAQ list can wait until someone opens Help.
Detail views load on tap. Fetch the list with excerpts, fetch the full body only when a row is selected. Smaller payloads, faster screens.
Offline: Cache Locally, Revalidate Cheaply
Mobile networks fail constantly — elevators, subways, dead zones. An app that shows a spinner because the FAQ endpoint timed out feels broken even though your API is fine.
The fix is two habits:
- Cache every successful response on device. SQLite, a file cache, or the HTTP client's built-in cache (
URLCacheon iOS, OkHttp's cache on Android). On screen open, show cached content immediately and refresh in the background. Stale FAQ beats no FAQ. - Revalidate with ETags instead of refetching. When the server sends an
ETagorLast-Modifiedheader, the client can ask "has this changed?" instead of downloading the body again:
curl -i "https://cms.example.com/api/v1/posts/getting-started"
# HTTP/2 200
# etag: "a1b2c3d4"
curl -i -H 'If-None-Match: "a1b2c3d4"' \
"https://cms.example.com/api/v1/posts/getting-started"
# HTTP/2 304
A 304 Not Modified has no body. On a list endpoint that's kilobytes saved per check, multiplied by every screen open on every device — real bandwidth and battery. URLSession and OkHttp both handle conditional requests automatically once the server sends the headers; you mostly just need to not disable it.
Images: Don't Ship 4 MB Originals to a Phone
The fastest way to wreck a content screen on mobile is to render the original upload. A 2400px, 4 MB JPEG in a 360px-wide card slot wastes the user's data plan and your scroll performance.
Two rules:
- Request a variant sized for the slot. Pick the variant closest to display width × device pixel ratio. A 360pt card on a 3x display needs ~1080px, not 2400px.
- Prefer WebP. iOS 14+ and effectively all Android versions in circulation decode WebP natively, and it's typically 25–35% smaller than equivalent-quality JPEG.
This is worth checking when you pick a CMS. UnfoldCMS converts uploads to WebP automatically and generates responsive sizes, so the API hands your app variant URLs instead of forcing you to resize on device or stand up an image proxy.
Below-the-fold images should lazy-load as the user scrolls, and your image library (Kingfisher, Coil, Glide) should cache decoded images to disk so the second visit costs nothing.
Auth: Nothing Secret Ships in the Binary
Anyone can unzip an APK or proxy an IPA's traffic. Every string in your build — API keys included — should be treated as public. That gives you two hard rules.
Rule 1: published content is read through public endpoints, no token. The content is public anyway; requiring a key just means shipping a key. UnfoldCMS's public read endpoints (posts, pages, categories, search, menus, settings) need no auth at all, so there's nothing in the binary to leak.
Rule 2: never embed a write token in an app. Write access to UnfoldCMS goes through Sanctum tokens, and those belong on servers — your backend, your CI pipeline, your sync scripts. A write token in a shipped binary is a defacement waiting to happen: one curious user with unzip and strings can edit your production content.
If you genuinely need restricted reads (member-only content, paid tiers), the gate is your own auth server: the user logs in to your backend, your backend fetches from the CMS server-side or issues short-lived per-user tokens. A shared secret in the build is never the answer.
Webhooks: Purge the Cache When Editors Publish
Cache TTLs create a staleness window. If your CDN caches the banner endpoint for an hour, an urgent banner change takes up to an hour to appear. Webhooks close that gap.
The flow: editor hits publish → CMS fires a webhook to your backend → your backend purges the affected CDN keys → the next app request gets fresh content from origin. Publish-to-live drops from "whenever the TTL expires" to a few seconds.
UnfoldCMS signs outgoing webhooks with HMAC-SHA256, so your endpoint can verify the payload actually came from your CMS before purging anything:
$expected = hash_hmac('sha256', $request->getContent(), $secret);
abort_unless(hash_equals($expected, $request->header('X-Signature')), 401);
Always verify. An unauthenticated purge endpoint is a free denial-of-service lever — anyone who finds it can empty your cache on a loop. The same publish-event-to-webhook pattern drives static site rebuilds too; we covered that end of it in how CMS webhooks trigger frontend rebuilds.
Push Notifications on Publish (the CMS Doesn't Send Them)
A popular pattern: new article goes live, users get a push. Be clear about who does what here — a CMS fires webhooks; it does not talk to APNs or FCM. No mainstream headless CMS sends push notifications itself, and any architecture that assumes it does will stall.
The working version is a small relay:
CMS publish event ──▶ webhook ──▶ your notification service ──▶ FCM / APNs
Your service receives the webhook, verifies the signature, checks the event type (you probably don't want a push for every FAQ typo fix — filter on category or a flag), builds the notification, and hands it to FCM or APNs.
This stacks nicely with scheduled publishing. Schedule the post for 9:00 a.m. in the CMS; the publish event fires at 9:00, the webhook fires, the push goes out — with nobody awake to press a button.
Pagination and Rate Limits
Two mistakes show up in almost every first integration:
- Fetching the whole collection. Don't request 500 articles to render a list showing 10. Paginate (
?page=2&per_page=20), read themetablock for totals, and load the next page when the user nears the end of the list. - Ignoring 429s. When the API rate-limits you, back off — exponentially, with jitter. A retry loop without jitter means every device that got throttled retries at the same instant, which is how you turn a rate limit into an outage.
Your cache layer is also your rate-limit insurance. If the app talks to a CDN and only misses reach the CMS, a traffic spike from a push campaign hits the edge, not your origin. Apps that call the CMS directly per-device are one viral moment away from finding the limits the hard way.
When a CMS Is the Wrong Tool
A CMS models editorial content: a handful of trusted writers, many anonymous readers, content that changes occasionally. The moment your data doesn't fit that shape, stop.
- User-generated content — comments, profiles, uploaded photos, reviews. This needs per-user auth, moderation, abuse handling, and write throughput. A CMS has none of that, and bolting it on fights the tool.
- Transactional data — orders, carts, payments, messages, anything per-user or real-time. This is application state, not content.
That's an app backend's job — Laravel, Rails, Supabase, whatever your team runs. Most real apps end up with both: the CMS serves editorial content through its read API, and a separate backend owns user data. Keeping them separate is the design, not a compromise.
FAQ
Do I need a vendor SDK to use a headless CMS in a mobile app? No. The API is plain HTTPS and JSON, so URLSession (Swift), Retrofit or Ktor (Kotlin), and Dio (Flutter) are all you need. Treat the CMS like any other REST service you'd integrate.
How do I update app content without an app-store release? Serve the content from the CMS API and fetch it at runtime. Editors publish in the CMS, and the app picks the change up on its next fetch — no build, no review queue, and old app versions get the update too.
Can the app show CMS content offline?
Yes, if you build for it: persist successful responses on device, render the cached copy first, and revalidate with If-None-Match when the network returns. The CMS doesn't do offline sync for you — the client-side cache is your code.
If you'd rather own the whole stack, UnfoldCMS is a self-hosted Laravel CMS with a public read API, signed webhooks, automatic WebP image variants, and scheduled publishing — the pieces this guide leans on. See how the API works in the docs or compare plans.
Free & Open Source
Own your CMS. No subscriptions.
Unfold CMS is free to download and self-host. Built on Laravel + React, full source code included.
Share this post: