React Native OTA Updates FAQ
What you can and cannot ship over the air, how store review policy applies, and what happens when native code changes.
The failures developers actually hit, and what causes them.
Nothing happens
I published an update but the app never changes.
Run
npm run ota:doctor first — it checks most of this for you. Otherwise it is almost always one of three things. One: you are running a debug build, which loads JS from Metro and never consults the OTA bundle path — you must test a release build. Two: the release's runtime version does not cover the installed native version, so the device is correctly told there is nothing for it (the check returns no_release_for_runtime). Three: the bundle override is missing from MainApplication/AppDelegate, so the app keeps loading the bundle baked into the binary. Check getCurrentPackage() — if it returns null after an install, it is the third.Which key goes in which build?
ota init prints one key per channel: ota_live_… for production, ota_test_… for staging. The key belongs to the channel, and the same key is used for both iOS and Android — the device reports its own platform. You can read the keys again any time from the project page in the dashboard.I built staging with the production key by mistake.
That build is a production build as far as the platform is concerned, and it will receive production releases. The channel is a property of the binary on purpose — nothing in JS or config can override it. Rebuild with the right key.
The update check succeeds but the download fails.
Check the device actually has network access to the internet — bundles download straight from the platform's storage, not through your app's own backend. A corporate proxy or VPN blocking outbound HTTPS causes exactly this symptom.
`The native module for '@otaupdate/react-native' is not available`
The native side was not rebuilt, or you are in Expo Go. A Metro reload is not enough — you need
pod install plus an Xcode build, or a Gradle build. In Expo, re-run npx expo prebuild --clean. If it persists in an Expo app, confirm the package still ships react-native.config.js; without it Expo drops the module from React Native autolinking and NativeModules.OtaUpdate is undefined.Updates that undo themselves
Every update rolls back on the next launch.
notifyAppReady() is never being reached. Either the new bundle crashes during startup, or the call sits behind a screen the user has to navigate to. Use withOtaUpdate, which calls it on mount, or move your manual call earlier.A release is stuck — devices refuse to install it again.
A hash that failed to boot is blacklisted on that device, and the SDK will not reinstall it. Publish a new release rather than re-publishing the same bytes. During development,
clearUpdates() wipes local state.What happens if the new bundle crashes immediately?
The launch that boots into a pending update marks it as loading. If the process starts again while it is still unconfirmed, the native layer reverts to the last confirmed bundle, blacklists the bad hash, deletes it, and reports
rolled_back. The user ends up back on a working app without doing anything.Rollouts
How are devices chosen for a partial rollout?
sha256(clientId + ":" + release) % 100 < rollout. Deterministic, so the same device gets the same answer on every check. Keyed on the release, so each release picks an independent slice rather than always hitting the same unlucky users.A device is outside the rollout. Does it get nothing?
No. The server walks from newest to oldest and serves the first release the device is both runtime-compatible with and inside the rollout for. Excluded devices still receive the previous stable release.
Can I change a rollout percentage after publishing?
Not on a published release — the percentage is fixed at publish time. Publish again at the wider percentage to reach more devices, or
ota rollback to pull the release back from devices that already installed it. Publishing less never uninstalls anything.What is the difference between promote and release?
ota release builds a fresh bundle from your working tree. ota promote takes the bytes a channel is already serving and serves them from another channel — no bundler run, no upload, and what you tested in staging is exactly what production gets.Scope and limits
Can I ship native code changes over the air?
No. Only the JS bundle and its assets. A new native module, a permission, an SDK bump or an
Info.plist change needs a store release. Shipping JS that calls a native API the installed binary lacks will crash the app — that is exactly what the runtime version is for.Does this violate App Store rules?
Apple's guidelines permit executable code downloaded to the JavaScriptCore/Hermes engine provided it does not materially change the app's purpose. Shipping bug fixes and UI changes is within that; shipping a different app is not. You are responsible for the content of what you push.
How large can a bundle be?
The server ceiling is 200 MB by default. Full bundles are sent every time — differential updates are not implemented, so keep an eye on size if your users are on metered connections.
What does the device send to the server?
The channel SDK key, the platform, the native app version, the release it is running, its native bundle identifier, and a random per-install UUID used for rollout bucketing and adoption counts. The UUID is generated on the device and stored in app data — not a hardware or advertising identifier. It resets if the app's data is cleared, which is why a reinstall counts as a new device.
Operations
Two CI jobs published at the same time. Did I lose a release?
No. Release numbers are allocated under a row lock on the project, so two publishes racing cannot claim the same number and both releases exist. The later one wins as the live release.
I republished the same bundle and got a 409.
Publishing bytes identical to the current release is refused, because it is nearly always an accident. Pass
--allow-duplicate if you meant it.The CLI refuses to publish because my working tree is dirty.
Deliberate: a bundle built from uncommitted code exists on no branch, so nobody can reproduce what shipped. Commit, or pass
--allow-dirty — the release is then recorded as dirty in its provenance.How do I rotate an SDK key?
On the project page, open the channel and choose Issue key — admin and above, and it asks you to confirm. Understand the consequence first: every already-shipped binary embeds the old key and will stop receiving updates on that channel until users install a new build from the store.
Is iOS verified?
Yes. Both platforms are verified end to end on Release builds — publish, staged rollout, in-process reload and rollback — on an Android emulator and an iOS simulator, with the on-device bundle hashes matching the published SHA-256 exactly.
Accounts and access
Why do I need a code every time I sign in?
Two-factor authentication is mandatory on this platform: every account, every login, with no opt-out and no "remember this device". The reason is what the platform does — anyone who can sign in can push JavaScript into your users' installed apps. A stolen password alone must not be enough for that.
My CI job can't sign in any more.
It should never have been signing in with a password. Create an API key (
ota api-key create github-actions) and set OTA_API_KEY. Keys are not subject to two-factor, are scoped to one organization, and cannot mint other keys or change membership — so a leaked one cannot escalate.The code never arrives.
Check the spam folder first. If it's genuinely missing, contact support — we can look up the delivery outcome for that login attempt.
I entered the wrong code too many times.
Five wrong guesses invalidate that code permanently — even the correct one stops working after that, which is deliberate. Start the sign-in again to get a fresh one. You can request up to 5 codes per 15 minutes, counted separately for signing in and resetting a password.
I've lost access to my email account.
There is no self-serve path — password recovery goes through that mailbox, so losing it is a full lockout. Contact your administrator, who can change the address on the account directly. This is the main practical cost of email-based two-factor.
Still stuck?
Run npm run ota:doctor first — it names the fix for most setup problems. Then set OTA_DEBUG=1 for CLI stack traces and watch adb logcat | grep OtaUpdate on device. The dashboard's Activity view shows whether the device ever reached the server at all.