React Native OTA Updates: How Production Delivery Works
An end-to-end engineering model for React Native OTA compatibility, artifact delivery, activation, observation, and recovery.
Written by George FeanFounder and maintainer of Bundle Drop
React Native over-the-air updates deliver a new JavaScript bundle and compatible assets to an application that users have already installed. The native iOS or Android binary does not change. A production OTA system therefore has one central obligation: select, deliver, and activate only an update that the installed binary can safely run.
The download is only one stage. Before a bundle becomes active, a release system has to establish native compatibility, build an identifiable artifact, publish it to a release track, select eligible devices, verify transferred bytes, stage or activate the result, observe application health, and recover when the candidate fails.
This guide follows that path end to end. It does not compare post-CodePush providers; Life after CodePush owns that decision. It also does not replace integration documentation. The goal is to make the architecture legible so a React Native team can evaluate any OTA implementation against the same production questions.
The shortest accurate model of React Native OTA
A React Native release has two relevant layers:
| Layer | Distributed by | Can change |
|---|---|---|
| Native binary | App Store, Google Play, enterprise distribution, or another signed native channel | Native modules, React Native runtime, JavaScript engine, permissions, entitlements, manifests, compiled configuration |
| OTA update | An update service used by the installed application | Compatible JavaScript, application logic, UI behavior, and assets that the native binary already knows how to load |
An OTA update cannot widen the native surface. If JavaScript calls a method that is absent from the installed binary, the fact that the bundle downloaded successfully does not make it compatible. The same applies to an asset that requires a loader or registration added only in a newer native build.
The useful test is concrete: can this exact production binary execute this exact bundle on this platform without a native capability introduced later? If not, build and distribute a new binary first. When an OTA update needs a store release provides the detailed change-classification framework.
The production OTA pipeline
The stages below answer different questions. Combining them into one “update available” state makes failures harder to diagnose and recovery harder to trust.
A controlled OTA release flow
1 · Prepare
Create an update for a known native runtime
Bundle JavaScript and compatible assets without assuming native capabilities that the installed binary does not contain.
2 · Verify
Publish to a test channel and exercise real release builds
Check startup, critical flows, bundle identity, and platform-specific behavior before production exposure.
3 · Release
Begin with a limited eligible audience
Use a staged percentage or deliberate target while keeping the runtime and channel boundaries explicit.
4 · Observe
Evaluate adoption, startup health, crashes, and product signals
A published update is not complete merely because an artifact was uploaded or downloaded.
Healthy
Expand the rollout deliberately
Increase exposure while the agreed health signals remain within their expected bounds.
Unhealthy
Pause, stop, or roll back
Limit further exposure and move affected installations toward a known-good compatible bundle.
| Stage | Question it answers | Safe failure behavior |
|---|---|---|
| Build | What JavaScript and assets belong to this release? | Do not publish an incomplete or non-reproducible artifact |
| Identify | Which exact artifact is this? | Do not reuse an identity for different bytes |
| Publish | Which project, platform, runtime, and channel own it? | Keep it unavailable when required metadata is incomplete |
| Resolve | Is this device eligible for this release? | Return no update rather than cross a compatibility boundary |
| Transfer | Can the device obtain the selected bytes? | Keep the current working bundle on interruption or corruption |
| Verify | Do the bytes match the selected artifact? | Reject the candidate before activation |
| Stage | Where is the verified candidate stored? | Preserve the active and embedded known-good states |
| Activate | When should the application start using it? | Follow explicit application policy rather than surprising the user |
| Observe | Is the candidate healthy on real devices? | Pause expansion when evidence is missing or unhealthy |
| Recover | How does the device or release track leave a bad update? | Return to an earlier compatible state without requiring the candidate to behave correctly |
The provider may implement these stages differently, but omitting one does not remove the underlying question. It usually moves the answer into application code, CI configuration, operator judgment, or an untested failure path.
Build one artifact that can keep its identity
An OTA artifact should be generated from an intentional source revision with the production bundler, engine settings, assets, and environment inputs for one platform. The matching source map should be preserved with it. Rebuilding “the same code” after testing can produce different bytes or different embedded configuration, which breaks the chain between verification and production observation.
A useful release identity survives the full path:
- CI builds the platform bundle and source map.
- The artifact receives an immutable identifier or content hash.
- Internal testing, staged rollout, crash reports, and rollback refer to that same identity.
- Operators can distinguish two OTA releases even when both run under the same app-store version.
This is why OTA observability cannot stop at versionName, CFBundleShortVersionString, or an app-store build number. Several JavaScript artifacts can run on top of one native version. Incident reports need the active OTA identity as well.
Compatibility is decided before delivery
Most production systems represent the native boundary with a runtime version or an equivalent target rule. A release and an installation must agree on that value before the update is considered eligible.
Expo describes runtimeVersion as the property that joins a build’s native code to compatible updates in its runtime version documentation. The specific policy differs by system, but the invariant does not: adding native code, changing generated native configuration, or moving to a different runtime requires a new binary and a new compatibility line.
Compatibility should usually be platform-specific. An iOS entitlement change does not necessarily alter Android, and the two store binaries can remain in circulation on different schedules. One shared application version is therefore not always enough evidence that the native surfaces match.
The JavaScript engine belongs inside this boundary. React Native ships Hermes together with React Native so the versions are tested as a compatible pair, as documented in Using Hermes. A team producing Hermes bytecode for OTA delivery should use the compiler and settings expected by the target binary rather than assuming bytecode is portable across runtime generations.
Runtime versions as compatibility lanes covers literal values, Expo authority, per-platform lines, and coexistence across old and new store binaries in more detail.
Release selection and artifact transfer are separate decisions
After compatibility establishes the possible release lane, a resolver can apply operational eligibility:
- project and platform identify the application surface;
- runtime version restricts selection to compatible binaries;
- channel identifies the intended release track;
- publishing state decides whether a candidate is live;
- audience rules may include or exclude a device;
- staged rollout may limit initial exposure to a stable cohort.
Only after those rules select a release should the system decide how to transfer it. A device with a suitable base artifact may receive a smaller patch. Another eligible device may require the complete bundle because its local history differs or because the patch is unavailable or inefficient. Both devices should converge on the same selected release.
Treating patch delivery as a transport optimization keeps release correctness independent of one delta path. Patch delivery and full-bundle reliability explains why the verified complete artifact remains necessary.
Verification happens before activation
Network success is not artifact success. A download may be interrupted, truncated, corrupted in local storage, served with unexpected metadata, or associated with the wrong release record. The client should verify the completed candidate against the selected artifact before it can replace the active bundle.
Compatibility and integrity answer different questions:
- Compatibility: is this release allowed to run on this native runtime?
- Integrity: are these the exact bytes the release system selected?
- Application health: does the verified, compatible update behave correctly after activation?
Passing one check does not imply the others. A bundle can be intact but incompatible. It can be compatible and intact but still contain an application bug. Production OTA needs a separate failure response for each case.
Activation timing is application policy
A verified candidate may activate immediately, on the next launch, or after an explicit application action. None of those modes is universally correct.
Immediate activation can be useful for an urgent correction, but restarting or reloading the application can interrupt user work. Next-launch activation is less disruptive, but adoption depends on a later restart. Manual activation gives product code control over prompts and readiness, but it also creates more states for the application to manage.
The release service should make activation behavior explicit. Download timing, activation timing, and rollout eligibility should not be inferred from one ambiguous “mandatory” flag.
Recovery has a server side and a device side
Server-side rollback changes what future update checks select. It can stop further exposure and direct eligible devices toward an earlier compatible release. It cannot reach an offline device or repair a startup loop before the application completes another check.
Device-side recovery addresses the candidate that has already become active. The client needs a known-good previous or embedded bundle and a health rule that decides when the new candidate is accepted. If startup repeatedly fails before acceptance, the device can return to that local state without waiting for the broken bundle to request help.
This mechanism has a visibility boundary. A native crash or a JavaScript failure before the OTA runtime initializes may not reach JavaScript-level health reporting. Native crash reporting and startup diagnostics remain necessary. Staged rollout and rollback connects release selection, stable cohorts, server rollback, and local recovery.
Expo and bare React Native use the same architectural boundary
Expo is not required for the OTA pattern. Bare React Native applications can integrate a native OTA client directly. Expo Prebuild or Continuous Native Generation projects can integrate through a config plugin, while expo-updates can be configured for EAS Update or another server implementing the Expo Updates protocol. Expo’s Updates documentation describes both the runtime-version requirement and custom-server configuration.
The integration workflow differs, but the production questions remain the same:
- Which native build contains the update client?
- Which runtime value represents its native surface?
- How does the app select a release track?
- When does it check, download, and activate?
- What known-good state remains when the candidate fails?
Changing providers or integration tooling does not remove these questions. It changes where their answers are configured and operated.
App-store policy constrains what OTA should do
An OTA mechanism does not grant permission to change an application without limit. Apple’s App Review Guidelines restrict downloaded code that introduces or changes application features or functionality. Google Play’s Device and Network Abuse policy restricts self-updating and remotely downloaded executable code and separately addresses interpreted languages loaded at runtime.
Teams remain responsible for the current policy text, the reviewed purpose of their application, and the behavior of every remotely delivered update. A conservative boundary sends native capabilities, permissions, and material platform changes through a new signed store binary. OTA handles compatible application changes inside that reviewed native surface.
A CI/CD review for React Native OTA releases
Automation should preserve evidence rather than turn every merge into an unobserved production release.
| CI/CD step | Evidence to preserve |
|---|---|
| Select source revision | Commit and dependency lock state |
| Resolve platform/runtime inputs | Platform, runtime version, engine, channel, and application environment inputs |
| Build bundle and assets | Deterministic command, logs, artifact, and source map |
| Verify before publication | Test results and the exact artifact identity |
| Publish to an internal track | Project, platform, runtime, channel, and resulting release identity |
| Promote or begin rollout | Approval, cohort size, and predefined stop conditions |
| Observe | Adoption, update failures, startup health, crashes, and product signals |
| Complete or recover | Expansion history, rollback action, and incident record |
Credentials should be scoped for the intended project and release action, stored outside source control, and rotated independently of application code. A production token should not be the easiest way to test a local command.
Where Bundle Drop fits in the pipeline
Disclosure: I am the founder of Bundle Drop.
Bundle Drop implements this model for bare React Native and supported Expo workflows. The public integration embeds project, channel, and runtime context in the application. Upload records the platform bundle under its compatibility line. Managed publishing can then apply staged rollout and optional audience targeting before a release is selected.
On the device, the client checks eligibility, downloads and verifies the selected update, stages it according to application policy, and retains recovery states. Patch delivery may reduce transferred bytes when the current device state has a compatible patch path; verified full-bundle delivery remains available otherwise.
Current configuration belongs in the operational documentation for installation, runtime versions, uploading, update policies, CI/CD, observability, and rollback.
React Native OTA update FAQ
What is a React Native OTA update?
It is a remotely delivered JavaScript bundle and set of compatible assets loaded by an already-installed React Native application. It changes the updateable application layer, not the compiled iOS or Android binary.
What can be updated without a new store build?
JavaScript logic, UI behavior, copy, and compatible assets may be eligible when the installed binary already exposes every native capability they require. Native modules, permissions, entitlements, manifest changes, engine changes, and other compiled configuration require a new binary.
Does React Native OTA require Expo?
No. Bare React Native can use a directly integrated native OTA client. Expo projects can use an Expo-compatible integration. In both cases, the team needs an explicit native compatibility boundary.
Are OTA updates available to every user immediately?
Not necessarily. Devices must check, download, verify, and activate the update. Connectivity, launch frequency, channel, runtime compatibility, targeting, rollout percentage, and activation policy can all affect adoption.
Can a patch be used for every update?
No. Patch delivery requires a compatible base and a useful delta. The complete verified bundle remains necessary for devices on other release histories and for cases where patching is unavailable or inefficient.
Does rollback undo a native change?
No. OTA rollback can select or restore an earlier compatible JavaScript bundle. A problematic native binary still requires the appropriate store or enterprise-distribution response.
A production OTA system is a release system
React Native OTA updates are often introduced as a faster download path for JavaScript. In production they are a compatibility-constrained release system. The architecture has to preserve one artifact identity, select only within the correct native runtime, verify before activation, separate rollout from transfer optimization, observe the running result, and recover without depending entirely on the candidate.
When those boundaries are explicit, OTA can shorten distribution for compatible changes without pretending that native releases disappeared. When they are implicit, the faster path becomes difficult to reason about precisely when the team needs it most.
About the author
George Fean
Founder and maintainer of Bundle Drop
George builds Bundle Drop and writes about the compatibility, delivery, rollout, and recovery boundaries behind React Native OTA updates.
GitHub profile