Over-the-air updates in Expo without a deployment service
Over-the-air (OTA) updates are the reason Expo is compelling. You write an app, ship it, then fix bugs in production the same day. But the infrastructure—the hosted update service—is another moving part.
We route around it.
The problem with app store releases
A user downloads version 1.0 from the app store. You fix a typo. That fix sits in your code until the next release, which might be weeks away. App store review is a speed bump. For a small team, this friction is killing.
OTA updates skip all of that. You deploy and users get it on their next app open. No approval delay. No waiting for the next scheduled release cycle.
The service everyone uses
The industry standard is EAS Updates, the hosted version from Expo. It is a managed service, which means you ship your update to their servers, and the app checks with their servers for new code.
For a small product, it is a great choice. For us, the hosted part felt like a step in the wrong direction: one more external dependency, one more API key, one more thing that could go down in the middle of the night.
So we built our own distributor.
The minimal approach
An OTA update is just a JSON manifest and a JavaScript bundle, hosted somewhere. A URL. The app fetches the manifest on startup, compares the version to what is installed, downloads the bundle if it is newer, and runs it.
The tradeoff is that the app cannot update itself until the next launch. No in-flight update while the user is in the middle of something. It is an acceptable tradeoff because updates are small and most users restart their apps multiple times a day anyway.
{
"version": "1.2.5",
"bundleUrl": "https://updates.example.com/bundle-1.2.5.js",
"releaseDate": "2024-10-15T15:30:00Z"
}
Hosting the bundles
Expo's expo export compiles your app to a static JavaScript bundle, suitable for offline loading. That output is just files. You can host it anywhere—S3, your own server, a CDN. No special infrastructure.
We publish to S3 and serve through CloudFront, the same way we serve everything else. The manifest is a single file that updates once per release. A cache header says "check back every 6 hours," so the app syncs often without hammering our servers.
The constraints
This approach requires that your app is open source or that you control the entire distribution. If you were shipping through the app store under someone else's review and you wanted to bypass store approval with OTA updates, Apple would not approve it. (We publish our own templates, so this is not a concern.)
It also requires that you are willing to deploy infrastructure. Our server is four lines of middleware. No rocket science. But it is not click-a-button convenience, either.
What we gained
We can fix bugs the day we find them. New features go out instantly. No review process. No version fragmentation—every user is on the same version of the codebase within a day or two.
The app still goes through the app store for the Expo runtime, so Apple and Google still vet the sandboxed code. What we bypass is the app store review process for JavaScript changes. That is a fair trade.
If OTA updates matter to your workflow, building your own is simpler than it sounds. It is a few hours of work and removes a moving part from your infrastructure.