# 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.

Canonical: https://bundledrop.app/resources/when-ota-update-isnt-enough-store-release

Published: 2026-07-05

Updated: 2026-11-01

Category: Guides

Topics: Compatibility, Expo

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.

For the complete release path around that decision, [React Native OTA Updates: How Production Delivery Works](/resources/react-native-ota-updates) follows a compatible update from build and selection through activation, observation, and recovery.

## 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](/docs/uploading) 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](/docs/expo) 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.

| Change | Typical delivery path | What to verify |
| --- | --- | --- |
| Copy, styles, or TypeScript business logic | OTA | No new native capability is referenced |
| JavaScript-only package update | OTA may be appropriate | Dependency has no changed native code or configuration |
| Add or incompatibly upgrade a native module | Store release | New binary contains the module and expected interface |
| Add a bundled image or font | OTA may be appropriate | Existing binary can load the format and needs no native registration |
| Add a camera or location permission | Store release | Platform declarations and native integration are present |
| Change an Expo config plugin option | Inspect generated native output | Rebuild if the plugin changes native projects |
| Change JS engine or React Native architecture | Store release | Runtime and generated native code have changed |
| Change API behavior or a server-side feature flag | Neither artifact may need release | Client already understands the response and behavior |
| Change an App Store or Play listing | Store-console operation | This 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](/docs/runtime-version) and [Runtime versions: the compatibility boundary for React Native OTA updates](/resources/runtime-versions-compatibility-boundary).

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. Establish a new identity for each affected platform: bump its manual runtime, or calculate its fingerprint with the changed native inputs and any explicit boundary.
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.

## Compatibility modes do not change the native boundary

In Bundle Drop's default manual mode, the team changes a platform runtime string when compatibility changes. Opt-in Beta native fingerprinting instead derives the identity from collected native inputs. Hybrid compatibility also includes an explicit platform boundary, so either a detected input change or a deliberate boundary change creates a different identity.

These modes change how the boundary is maintained, not what OTA can replace. Fingerprinting cannot add a native module to an installed binary, and switching it on requires a new native build. An unchanged hash is also not universal proof: custom compiler settings and other inputs outside coverage need explicit representation or a deliberate boundary change. See [choosing a compatibility strategy](/resources/native-fingerprints-runtime-versions) and the [fingerprint coverage guide](/docs/native-fingerprinting#coverage-and-custom-build-inputs).

## 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](/docs/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](/resources/staged-rollouts-targeting-rollback) 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.
