Guides

When an OTA Update Isn't Enough (and You Need a Store Release)

A practical boundary for deciding whether React Native and Expo changes can ship over the air or require a new native binary.

9 min read

An over-the-air update can change a great deal of a React Native application without waiting for a new store binary. That convenience makes one boundary easy to blur: an OTA release can replace JavaScript and bundled assets, but it cannot add native capabilities to a binary that is already installed.

This distinction matters because a change can look small in a pull request while still changing the native application. Adding a package, enabling a permission, or switching a JavaScript engine may each be a short edit. None becomes available to existing installations merely because JavaScript can be delivered remotely.

The practical question is not "is this feature written mostly in JavaScript?" It is:

Does the already-installed binary contain everything this update needs on the affected platform?

If the answer is yes, the change may be suitable for OTA delivery. If the answer is no, the team needs a new native build and a store release before it publishes JavaScript that relies on the changed native surface.

The installed binary defines the release boundary

Classify the change by the capabilities already present on the device—not by how small the source diff looks.

Compatibility question

Does the installed binary already contain every native capability this change needs?

Consider native code, modules, permissions, build configuration, runtime architecture, and platform-specific behavior.

OTA path

Publish JavaScript and compatible assets

Keep the affected platform on the same runtime line and use the normal OTA release controls.

Store path

Build and distribute a new native binary

Bump the affected platform runtime, ship the binary, then publish OTA updates for that new line.

Changing a runtime-version label cannot add native capabilities to an already-installed binary.

What an OTA update can actually replace

A React Native app has two relevant layers. The native binary contains the iOS or Android application, React Native runtime, JavaScript engine, linked native modules, platform permissions, entitlements, and compiled configuration. It also ships with an embedded JavaScript bundle and assets.

An OTA update replaces the updateable layer: JavaScript and assets that the existing native runtime already knows how to load. It does not compile native classes, change signed entitlements, or install a different engine.

This makes many everyday changes good OTA candidates:

  • application logic written in JavaScript or TypeScript;
  • copy, styling, validation, and state-management changes;
  • navigation or screen composition using components already supported by the binary;
  • feature-flag behavior and API integration changes;
  • JavaScript-only dependency updates;
  • images, fonts, and other bundled assets that existing native loaders and configuration already support.

Even here, "usually" is more accurate than "always." JavaScript can call a native API that an older binary does not contain, and an asset can require a loader that was never built into the app. File type alone does not establish compatibility.

The upload workflow packages a platform bundle and associates it with that platform's runtime version. Uploading is the delivery step. It is not a way to modify the native layer or to make an incompatible bundle safe for an older installation.

Changes that require a new binary

A store release is required when a change affects code, configuration, or capabilities compiled into the native app. Common examples fall into several groups.

Native code and native dependencies

Adding custom Swift, Objective-C, Kotlin, Java, C++, or Rust code changes the binary. So does adding a native module, removing one, or upgrading a package when that upgrade changes its native implementation or JavaScript-to-native interface.

React Native autolinking reduces manual integration work, but linking still happens during the native build. An installed app cannot compile a newly added native dependency.

Upgrading a package is therefore not classifiable from package.json alone. Some packages contain only JavaScript. Others include native modules, CocoaPods, Gradle dependencies, code generation, or platform configuration. A JavaScript-only upgrade can remain OTA-compatible; a native-affecting upgrade needs a new binary and, normally, a new runtime line.

Permissions, entitlements, and platform declarations

Changes to iOS entitlements, usage descriptions, associated domains, background modes, app groups, extensions, or URL handling are native build concerns. Android permissions, intent filters, services, providers, deep links, and other AndroidManifest.xml declarations are as well.

Some permission APIs can request access at runtime, but the binary must first declare and contain the corresponding platform capability. Shipping JavaScript that asks for a permission missing from the installed binary does not add the declaration.

The same principle applies when an Expo config value or config plugin writes to Info.plist, AndroidManifest.xml, an Xcode project, Gradle configuration, or another native file. The plugin may be authored in JavaScript, but its output belongs to the native project. That output takes effect only after prebuild or native generation and a new build. The Expo integration guide describes the corresponding rebuild requirement.

Runtime and build-system changes

Changing the JavaScript engine, upgrading React Native in a way that changes the native runtime, enabling a different architecture, changing native code generation, or modifying linked build settings creates a new execution environment. Existing installations retain the environment with which they were compiled.

Build configuration deserves the same scrutiny. A change to Metro or Babel may affect only generated JavaScript and remain compatible. A change to Gradle, CocoaPods, Xcode build phases, compiler settings, or a config plugin may alter the binary. Classify the output, not the tool or filename used to produce it.

A decision framework for ambiguous changes

Teams do not usually struggle with obvious Swift or Kotlin edits. The difficult cases sit near the boundary. A short review framework makes those decisions repeatable.

For each affected platform, ask:

  1. Does this change add, remove, or alter native code or a linked native dependency?
  2. Does it change a permission, entitlement, manifest entry, plist value, extension, native resource, or build setting?
  3. Does JavaScript begin using a native method, component, constant, event, or capability that older binaries do not expose?
  4. Would a clean native generation or build produce meaningfully different native projects or artifacts?
  5. Can the exact existing production binary run the new bundle without assuming anything from a future build?

Any "yes" to the first four questions is a strong signal that a store release is required. The fifth question is the final test. It should be answered for iOS and Android independently rather than inferred from the overall feature.

ChangeTypical delivery pathWhat to verify
Copy, styles, or TypeScript business logicOTANo new native capability is referenced
JavaScript-only package updateOTA may be appropriateDependency has no changed native code or configuration
Add or incompatibly upgrade a native moduleStore releaseNew binary contains the module and expected interface
Add a bundled image or fontOTA may be appropriateExisting binary can load the format and needs no native registration
Add a camera or location permissionStore releasePlatform declarations and native integration are present
Change an Expo config plugin optionInspect generated native outputRebuild if the plugin changes native projects
Change JS engine or React Native architectureStore releaseRuntime and generated native code have changed
Change API behavior or a server-side feature flagNeither artifact may need releaseClient already understands the response and behavior
Change an App Store or Play listingStore-console operationThis is storefront metadata, not an OTA bundle

Not every non-OTA change requires an app binary. Server-side changes can be independent of both delivery paths, while store descriptions and screenshots are managed through the stores.

Runtime versions mark the compatibility boundary

Bundle Drop uses runtimeVersion to identify the native interface against which an OTA bundle may run. An installation and an update must resolve the same platform runtime value. The detailed model is covered in Runtime Version and Runtime versions: the compatibility boundary for React Native OTA updates.

The important limitation is that changing the value does not upgrade a device. A runtime-version bump is a label for a new compatibility line, not a mechanism that installs native code.

For example, labeling JavaScript with a new iOS runtime can prevent an older iOS binary from receiving it, but no device has that new native capability until users install the corresponding store binary. Review iOS and Android separately: bump only the platforms whose native surface changed.

The safe native-change sequence

When a change crosses the binary boundary, use an order that never exposes old installations to JavaScript they cannot run:

  1. Make the native-affecting change and identify which platforms are affected.
  2. Bump the runtime version for each affected platform.
  3. Build and test the new native binary from that runtime configuration.
  4. Distribute the binary through the appropriate store or testing track.
  5. Publish OTA updates that require the new native surface only to the new runtime line.
  6. Continue supporting the previous runtime while meaningful numbers of old binaries remain installed.

The embedded JavaScript bundle in the new binary should be usable on first launch. An OTA update can then improve that runtime line, but it should not be the only thing making the newly installed build coherent.

Store review and user adoption create a period in which old and new binaries coexist. The same release channel may contain compatible updates for multiple runtime lines. Old and new runtimes each receive eligible bundles for their own line.

This coexistence is normal. Do not delete the old compatibility path simply because the replacement binary is available in a store. Users update on different schedules, managed devices may lag, and platform releases may reach production at different times.

Rollout and rollback do not repair incompatibility

Progressive delivery reduces the number of devices initially exposed to a change. It does not make an incompatible update compatible. A one-percent rollout of JavaScript that calls a missing native module still sends the wrong artifact to that one percent.

Rollback is also a recovery mechanism, not a substitute for correct runtime classification. Bundle Drop can move an installation back to an earlier compatible OTA bundle or its embedded native bundle, as described in Rollback. That is valuable after an application-level defect. It should not be used as permission to publish against an uncertain native boundary.

Once compatibility is established, the staged-rollout and rollback model explains how controlled exposure and recovery compose without changing that boundary.

The safest failure is to offer no update to an incompatible runtime. The installed app continues using its embedded or last compatible bundle until a suitable update exists.

Common mistakes

Treating every dependency change as OTA-safe

A lockfile does not reveal the whole artifact boundary. Check whether the package ships native code, participates in code generation, modifies platform configuration, or changes the interface used by JavaScript.

Treating every Expo config change as JavaScript

Expo app configuration serves several purposes. Some values are available to JavaScript or update manifests; others drive prebuild and alter native projects. Config plugins in particular can write native files. Inspect the evaluated configuration and generated native difference rather than assuming that app.json means OTA.

Bumping runtimeVersion and publishing immediately

A new runtime with no distributed binary has no installed audience. More seriously, labeling a bundle with a new runtime does not place the needed capability on a device. Build and distribute the corresponding binary before relying on the line.

Using one runtime for asymmetric native changes

If only one platform changed, forcing both onto a new line creates avoidable fragmentation. If both changed, updating only one creates an unsafe boundary. Review iOS and Android separately.

Assuming assets are always harmless

Replacing an image used by existing JavaScript is usually straightforward. Adding a format, font behavior, asset catalog entry, icon set, or loader that depends on native configuration may require a binary. Ask whether the installed app already knows how to consume the asset.

Using OTA delivery to avoid platform policy

Technical compatibility is only one constraint. Teams remain responsible for App Store and Play policies and should not use remote updates to bypass review requirements or materially change an app in a way the applicable platform rules do not allow.

Keep the question about the installed binary

OTA delivery is well suited to changes that fit within a native capability surface already deployed to users. Store releases extend or alter that surface. Runtime versions keep the two generations separated while adoption takes place.

The useful rule is simple even when the implementation details are not: take the exact binary currently installed on a user's device and ask whether it already contains everything the new JavaScript and assets require. If it does, OTA may be appropriate. If it does not, create a new platform runtime, ship a native build, and publish compatible OTA updates only after that binary exists.

That boundary is not a limitation to work around. It is what makes remote delivery predictable while native applications, store releases, and older installed versions continue to coexist.

Interested in safer OTA deployments?

Read the implementation guides or connect a React Native project when you are ready to test the release workflow.