NNabeel Hassan

Blog · October 4, 2026 · 8 min read

Your Old App Versions Never Die: Designing a Backend for Mobile Clients You Can't Force-Update

By Nabeel Hassan — AI Engineer · ICPC World Finalist

TL;DR: A web deploy replaces every user's version at once. A mobile release does not. Once you ship through the App Store and Play Console, you are running a population of app versions in the wild, and some of them will stay there for months. The backend has to serve all of them. The rules I follow: every request says which app version sent it, API changes are additive only, clients tolerate data they do not recognise, a minimum-version gate and remote flags ship in version one, business logic stays on the server, and old production builds get tested against every backend change. I learned most of this shipping React Native, Unity and web clients against one backend as CTO at RAQTS, and from building an SDK that lived inside other people's apps at Geonode.

A bug fixed in a Next.js portal is live for everyone within minutes. A mobile app breaks that assumption quietly: the release you shipped today joins the ones you shipped last month, and they all keep talking to the same server.

Why old app versions never really die

At RAQTS, where I stepped in as CTO of a sports-tech platform, one product lived in three runtimes: a Unity interactive layer, React Native mobile apps and a Next.js web portal. The portal was the easy one. The mobile apps were where compatibility became a permanent part of the job, for reasons that have nothing to do with code quality:

The result is a long tail. Look at the version breakdown in your analytics a few weeks after any release and you will usually find several versions still active. Each of them was built against the API as it existed on the day it shipped, and each of them will call your server exactly the way it always did, forever, until the user updates or deletes it.

So the right mental model is not "the app" and "the backend". It is one backend serving a set of clients you no longer control, each frozen at a different moment in your API's history.

Rule 1: every request says which version sent it

The cheapest thing you can do, and the one I would not ship a first release without, is to have every request carry the app version, build number and platform, usually as headers. It costs nothing on day one and it is impossible to add retroactively, because the versions already in the wild will never send it.

With it, questions like "which versions still call this endpoint?" or "can we remove this field yet?" have answers instead of guesses.

Rule 2: change the API by adding, not by editing

The core discipline is that the API only ever grows. In practice that means:

This is sometimes called expand and contract: expand the API so old and new clients both work, migrate clients, then contract once the old path is genuinely dead. The contract step is the one teams rush. On mobile it should be the slowest step you take.

I reach for a versioned endpoint, a /v2 of something, only when a change truly cannot be additive, because it means maintaining two code paths. The GraphQL dashboard work at Bettershop taught me the same thing on the web: a schema many consumers depend on rewards growing it carefully over redesigning it.

Rule 3: clients must tolerate what they do not understand

Additive changes only work if the clients already in the field can survive them. That has to be built into the very first version, because you cannot teach a shipped binary new manners.

Concretely, a well-behaved mobile client:

Rule 4: the minimum-version gate ships in version one

The pattern I use is a small remote check that runs on launch. The app asks the server for two numbers: the minimum supported version and the recommended version. Below the recommended version, the app nudges the user to update and lets them carry on. Below the minimum, it blocks and sends them to the store.

This is your emergency brake, and it has the same catch as the version header: it only works for versions that contain it. If your first release ships without the gate, that release can never be forced to update, and you will be supporting it until the users leave on their own. Building it in from version one is a few hours of work that buys you years of options.

A hard block is a bad experience, so I keep it for real emergencies and rely on additive design for everything else.

Rule 5: remote flags so features can be switched off without a review

Since a shipped binary cannot be hot-fixed, the next best thing is being able to turn parts of it off remotely. New features go out behind a server-controlled flag, and the flag can be scoped by version and platform.

That turns a bad release from "wait for the next review and hope" into "switch the feature off now, fix it properly, and ship calmly". It also lets a feature land in a build before it is turned on, which takes the store review calendar out of your launch date. It is the same idea as treating a live agent's behaviour as something you change carefully and can roll back, which I wrote about in regression testing voice agents. Anything that is expensive to redeploy deserves a cheap off switch.

Rule 6: keep the logic on the server

The more decisions an app makes locally, the more versions of those decisions you end up running. If pricing rules, eligibility checks or ranking logic live in the client, a bug in them lives in every version that shipped with it, and the only fix is a release people may never install.

At RAQTS the clearest case was the live leaderboard. The backend owns the ordering, always, and every client, Unity, mobile or web, renders what the server says. One reason that rule mattered is consistency: three runtimes have to show the same standings. The other is compatibility. When ordering logic lives in one place, fixing it fixes it for every app version at once, including the ones that will never be updated.

The general rule: clients present, servers decide. Thin clients age far better than smart ones.

The extreme case: code inside someone else's app

At Geonode I built the Repocket C# SDK for Windows, Android and iOS. An SDK is the hardest version of this problem, because you do not even control the release of the app it lives in. The host app decides when to update its dependencies, and its users decide when to update the host app. Your code can be several layers removed from anyone who can push a fix.

That pushes every rule above further. The SDK has to identify its own version to the server, the protocol has to be tolerant in both directions, and anything that might need to change later has to be controllable from the server side. The same instinct shows up in the React Native VPN I built there, where native code the OS controls keeps running on its own terms. Software that runs out of your reach has to be designed assuming you will never get to touch it again.

Rule 7: test the old builds, not just the new one

Most teams test the new app against the new backend and stop there. The combination that actually breaks in production is the old app against the new backend.

So I keep recent production builds installable, which is much easier when the pipeline archives every release artefact, a habit I covered in building a Unity CI/CD pipeline. Before a backend change that touches a shared endpoint goes out, the oldest supported version gets a smoke test against staging: launch, sign in, the core flows, the screens that read the changed data. It takes minutes, and it is the only test that matches what your slowest-updating users will experience.

A checklist for your first mobile release

If you are about to ship a mobile app for the first time, these are the things that are cheap now and impossible later:

None of this shows up in a demo. It is the part of shipping to app stores that decides whether a backend deploy on a Tuesday afternoon is a non-event or a support queue full of people on last month's version. In my fractional CTO work, it is one of the first things I check on a mobile product, because it is far cheaper to set up before launch than to retrofit after.


I lead engineering as a fractional CTO and build cross-platform products across Unity, React Native, Next.js and AI agents, with release practices that survive real users. If you are about to ship a mobile app or inherit one, more about my background here, or book a call.

FAQ

Why do old mobile app versions keep calling my API?

Because a mobile release does not replace the installed base the way a web deploy does. Store review delays, phased rollouts, users who turn off automatic updates and phones that cannot run your newest build all leave older versions active for months, and each one calls the API exactly as it did the day it shipped.

How do you change an API without breaking old app versions?

Make changes additive: add new fields instead of renaming, never change a field's type or meaning, give new inputs server-side defaults, and only remove something when version data shows no supported app still reads it. Reserve a new versioned endpoint for changes that truly cannot be additive.

How do you force users to update a mobile app?

Build a launch-time check that asks the server for a minimum supported version and a recommended version, nudging users below the recommended one and blocking those below the minimum. It only works for versions that include it, so it has to ship in your very first release.

Should business logic live in the mobile app or on the server?

On the server wherever possible. Logic in the client is frozen into every version that shipped with it, so a bug can only be fixed by a release people may never install. When the server decides and the app only presents, one backend fix reaches every version at once.

Building something in this space?

I take on AI-agent, automation and product work directly — scoped fast, shipped fast.

Book a discovery call →

Keep reading