Skip to content

Mobile Backends: You Can't Force an Update

Most advice about mobile backends is just backend advice. What actually makes them different is that the client is a version of your app from two years ago that you cannot change.

Share
A bright central cube connected by unbroken lines to many smaller cubes, near ones sharp and bright, distant ones dim and faded

Search for advice on building backends for mobile apps, and you get a backend checklist. Scale horizontally, secure your endpoints, design good APIs, choose a database, add caching, monitor everything.

All true, and none of it is about mobile. Swap "mobile app" for "web application", and the article doesn't change, which means it isn't answering the question it asked.

What actually makes a mobile backend different comes down to one constraint, and almost everything else follows from it: you cannot make the client update.

The constraint

On the web, you deploy, and everyone gets the new version on their next page load. The client and server move together, always.

On mobile, they don't. Someone installed your app eighteen months ago, disabled automatic updates, and hasn't opened the store since. Their copy is still calling your API, and it will keep calling it until they delete it. Even for users who do update, there's a review delay before a release exists and then an adoption curve measured in weeks.

So your backend isn't serving one client. It's serving every version of your client that anyone still has installed, simultaneously, forever.

That reframes several decisions that look optional in a web context.

API versioning stops being a nice-to-have

On the web you can change a response shape and update the JavaScript in the same deploy. On mobile, that same change breaks every installed copy that expected the old shape.

The working rules:

Additive changes only, within a version: Adding a field is safe, since older clients ignore what they don't recognise. Removing a field, renaming one, changing a type, or changing the meaning of an existing value are all breaking changes, however small they look.

Version explicitly, whether in the path (/v2/orders) or a header. Pick one and be consistent, because otherwise you'll discover the contract by reading client code.

Expect to run several versions at once, and decide deliberately how long you'll support each. A published policy, something like "the two most recent major versions plus twelve months," is better than the implicit policy of supporting everything forever, which is what happens by default.

Build a forced upgrade path before you need one: Every mobile app should have an endpoint the client checks on launch that can say "this version is no longer supported, here's a store link." Adding that after you have a million installs is much harder than shipping it in version one. Eventually, you will need it, either for a security fix or because supporting a five-year-old contract has become untenable.

Instrument version usage. You cannot make retirement decisions without knowing what proportion of traffic each version accounts for.

The asymmetry, and how to exploit it

Here's the useful consequence of the same constraint. Server changes ship in minutes. Client changes take review time plus an adoption curve, and never reach everyone.

Which means: put logic on the server wherever you reasonably can, because that's the part you can fix.

Business rules, validation thresholds, pricing, eligibility, copy that might change, the ordering of a list. Every one of those hard-coded in the app is a decision you've frozen for a year. The same rules on the server are a deploy away.

Remote configuration and feature flags follow from this and are close to mandatory on mobile. Ship the code dark, enable it server-side, and turn it off when it misbehaves without waiting for a review cycle. The alternative is an emergency release, and emergency releases on mobile aren't fast.

There's a limit worth naming: pushing everything to the server makes the app useless offline, which is the other half of the problem.

The network isn't there

Web applications assume connectivity. Mobile apps run on the underground, in lifts, in rural areas, on a train passing through a tunnel, and on a phone that switches from wifi to cellular mid-request.

Designing for that changes the backend, not just the client.

Writes need to be idempotent: A request that times out may or may not have been processed, so the client will retry, and a duplicate order is worse than a failed one. Accept a client-generated idempotency key on every mutating endpoint and return the original result on a repeat. This is the single highest-value thing on this list.

Sync needs a model, chosen deliberately: Server-authoritative is simplest: the server decides, and the client conforms. Last-write-wins is easy and silently loses data. Field-level merging is better when changes usually affect different fields. Conflict-free replicated data types are correct and expensive, and worth it for genuinely collaborative editing and rarely otherwise.

Deltas beat full payloads: A client returning after a week wants what changed, not everything. A cursor or updated-since parameter turns a large sync into a small one, and it needs to be in the API design from the start.

Clocks lie. Device time is user-settable and often wrong. Timestamp on the server, and treat any client-supplied time as untrusted input.

Battery and radio

A constraint with no web equivalent, and one that gets you removed from a phone.

Every network request wakes the cellular radio, which then stays in a high-power state for several seconds afterwards. Ten requests spread over a minute cost far more battery than ten requests sent together, even though the bytes are identical.

So chattiness is a backend design problem. Endpoints that force a client to make five calls to render one screen are burning battery, and the fix is on your side: give the client an endpoint that returns what the screen needs.

Push instead of polling: An app polling every thirty seconds to check for updates is the clearest way to drain a battery and get uninstalled. Push notifications exist so the server can tell the client something changed.

Batch and coalesce: Queue analytics events, telemetry, and non-urgent writes on the client and flush them together.

Respect payload size: Mobile data costs money in a great many markets. Use compression, select fields sensibly, and don't return fifty fields when the screen shows three.

Shaping responses for screens

The generic-API instinct, one resource per endpoint, composed by the client, is more expensive on mobile than on the web because each round trip costs latency, battery, and reliability.

The backend-for-frontend pattern is the usual answer: a thin service that exists specifically to serve your mobile clients, composing internal calls into one response shaped like a screen. It couples your API to your interface, which is a real cost, and it's usually worth paying when the alternative is five round trips on a cold connection.

GraphQL solves the same problem differently, letting the client ask for exactly the fields it needs, which is genuinely useful when multiple client versions want different shapes of the same data. The costs are real too: caching is harder, query cost control becomes your problem, and the flexibility can mask expensive queries.

Either way, the principle holds: design endpoints around what a screen needs, not around your database tables.

Authentication on a device

Mobile sessions are long-lived in a way web sessions aren't. Nobody wants to log into an app weekly, so tokens live for months, and that changes the design.

Short-lived access tokens with long-lived refresh tokens, with the refresh token rotated on every use. If a rotated token is presented twice, that signals it was stolen, and the whole family should be revoked.

Store tokens in the platform's secure storage, meaning the iOS Keychain or the Android Keystore, not in shared preferences or a plain file.

Plan for the offline gap: A device that hasn't connected for six weeks will present an expired access token and an old refresh token. Decide what happens then, because the default behaviour is usually silently logging the user out.

Revocation has to work: Long-lived sessions mean a compromised token is valuable for a long time, so you need a way to invalidate one server-side.

On certificate pinning: it genuinely defends against interception, and it has bitten a lot of teams, because a certificate rotation with pins baked into an old app version bricks that version entirely. If you pin, pin to the intermediate CA rather than the leaf, ship backup pins, and make sure your rotation process accounts for clients you can't update.

And the client is less trusted than on the web. An installed application can be decompiled, inspected, and modified. Anything embedded in it is public, and every rule that matters must be enforced server-side, which is the same trust-boundary argument I made in the piece on front-end security.

Push notifications, honestly

Push goes through Apple's and Google's services, which means it isn't your delivery guarantee.

Treat delivery as best-effort. Notifications get dropped, delayed, batched, or suppressed by the operating system's power management. Anything that must arrive needs to be fetchable when the app next opens, so push becomes a hint that something changed rather than the transport for the change itself.

Device tokens also rotate on reinstall, on restore to a new device, and occasionally for no visible reason. Your backend needs to accept token updates, deduplicate them per user, and prune the ones the push service reports as invalid, or you accumulate a table full of dead tokens and a falling delivery rate that looks like a bug elsewhere.

What isn't special

Worth saying plainly, since the original checklist spent most of its length here.

Scaling, database choice, and caching work the same for mobile backends as for anything else. Postgres by default, cache as far out as correctness allows, and don't reach for microservices because you read that they scale. Those are covered properly in the pieces on server-side tools, caching, and the architecture pendulum.

The only caching wrinkle specific to mobile is that your clients cache too, on device, for much longer than a browser would, so an aggressive server-side TTL means less than you'd think once the app has its own copy.

The short version

Your backend serves every version of your app that anyone still has installed, and you cannot change that. Version your API explicitly, make only additive changes, and ship a forced-upgrade endpoint before you need it.

Put logic on the server, because that's the half you can fix quickly.

Make every write idempotent with a client-supplied key, since retries over unreliable networks are guaranteed.

Design endpoints around screens rather than tables, and treat every extra round trip as a battery cost.

Then treat scaling, caching, and database choice as ordinary backend work, because that's what they are.