Over-the-air updates let a React Native or Expo app ship a new JavaScript bundle to users without going through app store review. For a small team that is the difference between fixing a bug in an hour and waiting a day or more, and it is why most React Native apps use some form of it. It also means the code your users run can change after review, delivered by infrastructure you control, and that raises three security questions: can anyone else push code to your users, does the update stay inside what the stores allow, and what happens when an update breaks.
Short answer
Over-the-air updates are allowed for interpreted JavaScript as long as they do not change the app's primary purpose or add features that would need review, and they become a security risk when the update channel is not authenticated end to end. Use a maintained update service, turn on code signing so the app verifies every update with a public key it ships with, keep the signing key out of CI logs and shared machines, restrict who can publish to production channels, and keep a fast rollback. Microsoft's CodePush service has been retired, so apps still pointing at it need a replacement rather than a patch.
How over-the-air updates work
A React Native app has two layers: native code compiled into the binary that the store reviews, and a JavaScript bundle that the native layer loads and runs. An update service hosts newer bundles. At launch or in the background, the app asks the service whether a newer bundle exists for its runtime version, downloads it, and loads it on the next start.
EAS Update is the most widely used service for Expo and React Native apps today. Microsoft's CodePush was the other common choice for years; its hosting was part of Visual Studio App Center, which Microsoft retired after March 31, 2025, and the react-native-code-push repository was archived in May 2025. Self-hosted servers that implement the same protocols exist too.
The security model in all of them comes down to one thing: the app runs whatever bundle the update channel delivers.
What the store rules allow
Apple's position is set out in two places. App Review Guideline 2.5.2 says apps may not download, install or execute code which introduces or changes features or functionality of the app. Apple's developer agreement then makes an exception for interpreted code, such as JavaScript run by the platform's engine, provided it does not change the primary purpose of the app or add features inconsistent with what was reviewed.
In practice that means bug fixes, copy changes, small UI adjustments and tuning are routinely shipped over the air, while a new feature set, a new business model or anything a reviewer would want to see should go through review. Google Play's policies take a similar view of apps that change their behaviour after installation in ways that would have affected review. Using updates to slip a feature past review is a policy risk to the whole developer account, not just to one release.
The threat model in plain terms
| Risk | How it happens | Impact | Control |
|---|---|---|---|
| Unauthorised publish | Leaked token or compromised CI pushes a malicious bundle | Arbitrary JavaScript on every user's device | Code signing, scoped tokens, protected channels |
| Tampered delivery | Bundle altered between server and device | Same as above | Code signing verified on device |
| Policy breach | Update adds reviewable features | Store removal, account action | Release rules for what ships over the air |
| Broken update | Bad bundle crashes on launch | Users locked out until fixed | Staged rollout, automatic rollback |
| Runtime mismatch | Bundle expects native code the binary lacks | Crashes or silent failures | Runtime versioning |
The first two rows are the security core. Every user's app trusts the update channel completely, so the update channel is effectively a production deploy pipeline to every device you have.
Code signing is the control that matters
Transport encryption protects the download from casual interception, and it does not prove who produced the bundle. If an attacker obtains your publishing credentials, or compromises the hosting, a TLS-protected download faithfully delivers their code.
End-to-end code signing closes that gap. The app ships with a public key inside the reviewed binary. Every update is signed with the matching private key before publishing, and the app refuses to load any bundle whose signature does not verify. Expo documents this as end-to-end code signing for EAS Update, including how key rotation works. With signing on, a compromised hosting account or intercepted download cannot push code that runs, because the attacker does not hold the private key.
This maps directly to CWE-494, download of code without integrity check, which is the weakness an unsigned update channel exhibits.
Protecting the signing key
Signing moves the trust to the private key, so treat it like a release signing key:
- Keep it out of the repository and out of CI logs.
- Store it in a secrets manager or hardware-backed store, and limit who can use it.
- Sign in a dedicated release job rather than on developer laptops.
- Plan rotation in advance, since apps in the field only trust keys their binary knows about.
Access control on publishing
Even with signing, the people and systems that can publish to production deserve the same care as production server access. Use separate channels or branches for development, preview and production. Restrict production publishing to a release job, with tokens scoped to that purpose. Require review on the commits that feed a production update, as you would for a server deploy. Keep an audit trail of who published what and when.
A common weak point is a long-lived personal access token used in CI, created by a developer who has since left. Rotate tokens, prefer short-lived credentials where the service supports them, and review access when people change roles.
Rollback and staged rollout
An update that crashes on launch is a security problem as much as a reliability one, because users cannot reach the app to receive the fix if the fix is also delivered by that app. Two practices limit the damage. Roll updates out to a small percentage first and watch crash rates before widening. Use the update service's rollback so the app reverts to the last good bundle, or to the embedded bundle, when a new one fails to start.
Runtime versioning belongs here too. An update built for native modules the installed binary does not contain will fail in confusing ways. Tie every update to a runtime version that matches the binary, and ship native changes only through the stores.
Migrating off CodePush
Apps still configured for CodePush are pointing at a retired service. The practical path is to move to a maintained update service or a self-hosted server that is actively maintained, then ship a store release with the new client and keys before relying on it. Plan for a period where some users still run the old binary and receive no updates at all; a minimum version check that prompts those users to update from the store closes that gap. Use the migration as the moment to turn on code signing, since adding it later means another store release.
Checking your setup
Review the update configuration in the built app, not just the source: which update server it contacts, whether code signing is configured, which channel the production build follows, and whether a debug or development channel is accidentally enabled in a release build. An automated pass over the artifact, such as PTKD.com, flags build-level issues like debuggable flags, verbose logging and embedded secrets, which often travel together with update misconfiguration; the publishing permissions and key handling are an account and pipeline review you do directly. The broader OWASP MASVS code quality requirements set the expectation that apps only run code from trusted, verified sources.
Questions to answer for your app
- Which service delivers updates, and is it maintained?
- Is code signing on, and where is the private key?
- Who and what can publish to the production channel?
- What stops an update that crashes on launch?
- What is your rule for what may ship over the air and what must go through review?
What an incident looks like
If a publishing credential leaks, the first hour matters. Revoke the token, check the update history for any release you did not make, and if one exists, publish a known-good bundle immediately so devices replace the bad one on their next check. With code signing on, an attacker holding only a publishing token cannot get unsigned code to run, which turns a potential mass compromise into a credential rotation. Without signing, every device that fetched the malicious update ran it, and the only clean recovery may be a store release.
Write the steps down before you need them: who can revoke tokens, how to identify the last good update, how to publish it, and how to tell users if anything ran. Treat it the same way you would a compromised server deploy key.
Release rules that keep updates inside the guidelines
A short written policy prevents most store problems. A typical version allows over-the-air updates for bug fixes, text and translation changes, styling, and configuration within existing features, and requires a store release for any new screen or feature a reviewer has not seen, any change to payments, subscriptions or data collection, any new permission, and any change to native code. Review each over-the-air release against that list before publishing, and record the decision with the release.
Teams that follow a rule like this rarely have trouble at review, because the reviewed binary always reflects what the app actually does.
Monitoring updates after release
Treat each over-the-air release like a deploy that needs watching. Track adoption so you know how many devices run each update, compare crash and error rates between the new update and the previous one, and set an alert threshold that triggers rollback automatically or pages someone. Keep the release notes for every update, even small ones, because when a support ticket arrives a week later the first question is which bundle the user was running.
It is also worth checking that devices actually verify signatures in practice. Publish a deliberately unsigned test update to a development channel on a test device configured for that channel, and confirm the app refuses it. A misconfigured client that silently accepts unsigned updates looks identical to a correct one until someone tests it.
Updates and privacy declarations
An over-the-air update can change what data the app collects without touching native code, for example by sending a new analytics event or a new field to the backend. Store privacy declarations describe the app's actual behaviour, so a JavaScript-only change that adds collection still needs the App Store privacy details and the Google Play data safety form updated. Include that question in the release rules, next to the question of whether the change needs review.
Data safety and privacy details are a frequent cause of review questions, and keeping them accurate is far easier when every release, store or over-the-air, passes through the same short check.
What to take away
Over-the-air updates are allowed for interpreted JavaScript that does not change what the app is, and they turn the update channel into a direct path to every user's device. Authenticate that path end to end with code signing, protect the signing key like a release key, restrict publishing to a reviewed release job, and keep staged rollout and rollback ready. If the app still depends on CodePush, replace it with a maintained service and turn on signing as part of the move. Keep feature changes that a reviewer would want to see in store releases.




