Why most OTA setups fail the first real emergency
Teams add expo-updates in week one, publish a couple of bundles, and call it done. Then a checkout bug hits production on a Friday night, and the "fix it in five minutes" promise turns into two hours of wondering which build is listening to which channel, whether the fix is compatible with the installed binary, and why half the users still have the old code. The goal of OTA isn't "can push JS". It's time-to-fixed-user. Everything below optimises for that number.
The five must-haves
-
Separate channels per environment. At minimum
previewandproduction, mapped in eas.json build profiles. Every binary should know exactly which channel it listens to, and nobody should be able to reach production users by accident from a laptop. -
A runtime version policy you can trust. Use
runtimeVersion: { policy: "fingerprint" }so any native change (a new module, a config plugin, a permission) automatically produces a new runtime version. An OTA bundle then only reaches binaries that can actually run it. Hand-bumped version strings are how you ship JS that calls a native module that isn't there. - Staged rollouts. Push to 10% of production, watch crash rates and key events for an hour, then widen. A bad bundle to 10% of users is an incident. A bad bundle to 100% is a postmortem.
- A one-command rollback. Know before you need it: republish the previous good update, or roll back to the bundle embedded in the binary. Write the command in your README. At 11 PM nobody wants to read CLI docs.
- Error recovery plus crash reporting tied to the update ID. expo-updates can fall back when a new bundle crashes on launch, but you still need Sentry (or similar) tagging every event with the update ID and channel. Otherwise "crash rate went up" can't be traced to "update X caused it."
The five mistakes to avoid
- One channel for everything. If staging and production binaries share a channel, your QA build is your production build. Split them on day one.
- OTA-ing native changes. Upgraded a library with native code? That needs a store build. The fingerprint policy guards this, but only if you don't override it "just this once."
-
Forcing reloads mid-session. Calling
reloadAsync()the moment a download finishes wipes forms, carts and scroll position. Apply on next cold start by default, and reserve immediate reloads for critical fixes behind a friendly prompt. - Skipping code signing. Signed updates mean the app rejects any bundle your key didn't sign. It takes a certificate and two config lines. Do it before you have users, not after an incident.
- Treating OTA as your release strategy. OTA is for fixes and small iterations. Shipping major features over the air bypasses review in ways store guidelines frown on, and it trains the team to skip proper releases. Ship features in binaries; use OTA for fixes.
The bad-bundle test
Publish an update to your preview channel that throws on launch. Then check four things: the app recovers to the last working bundle, your crash tool shows the error tagged with the update ID, you can roll back with one command, and the next good update reaches the device. If any step needs Slack archaeology or a store resubmission, your OTA setup is a demo, not a safety net.
A pragmatic 2026 OTA config
- Updates: expo-updates with EAS Update, Expo SDK 53+.
- Runtime version: fingerprint policy, no manual overrides.
-
Channels:
previewandproduction, bound per build profile in eas.json. -
Check strategy: check on launch, apply on next cold start;
useUpdates()for an optional "Update ready, restart?" banner. - Safety: code signing enabled, staged rollouts on production, rollback command in the README.
- Observability: Sentry with the update ID and channel as tags on every event.
-
CI: GitHub Actions publishes to
previewon merge to main; promotion toproductionis a manual, reviewed step.
What's next
The best OTA setup is the one you've already rehearsed. Every team eventually ships a broken bundle; the ones that recover in minutes are the ones who tested rollback on a quiet Tuesday. RapidNative ships with these OTA defaults wired in: channels, fingerprint runtime versions, and rollback scripts. Grab it or wire your own. The rehearsal matters more than the repo.
Top comments (0)