Comparisons

Life after CodePush: choosing an OTA strategy for React Native

A technical framework for comparing Expo Updates, managed OTA platforms, and self-hosted infrastructure after App Center CodePush.

10 min read

App Center CodePush gave React Native teams a durable release model: ship native binaries through the stores, then deliver compatible JavaScript and bundled assets over the air. The service was retired with most of Visual Studio App Center on March 31, 2025, according to Microsoft’s App Center retirement notice. Microsoft now points CodePush users to a standalone, self-hosted CodePush server, while the original React Native client repository remains available as an archived project.

That change does not remove the need for OTA delivery. It turns a previously implicit platform choice into an architecture decision. A team can adopt Expo Updates, use a managed OTA provider, or operate compatible infrastructure itself. The right answer depends less on feature checklists than on compatibility authority, release controls, recovery behavior, observability, and the amount of infrastructure the team wants to own.

Map the release concepts before migrating the tooling

The labels change between systems, but the durable concerns are release-track selection, native compatibility, and application timing.

CodePush concept

Deployment key

Selects a CodePush deployment

Portable concern

Channel selection

Selects a named OTA release track

CodePush concept

Target binary range

Limits compatible native app versions

Portable concern

Runtime version

Defines the native compatibility boundary

CodePush concept

Install mode

Controls when an update is applied

Portable concern

Update policy

Separates check, stage, and apply behavior

Start with the boundary, not the vendor

An OTA system can replace JavaScript and assets that the installed native binary already knows how to run. It cannot safely introduce a native module, change an iOS entitlement, add an Android permission, switch the JavaScript engine, or apply another change that belongs in a new store binary.

Every strategy therefore needs an explicit compatibility boundary:

  • CodePush used a target binary version or semver range to decide which app-store versions could receive a release. Its official CLI documentation describes that gate as protection against sending an update to an older or newer, potentially incompatible binary.
  • Expo Updates uses a runtime version. Expo’s Updates documentation states that a remote update targets one runtime version and can only load in a binary with a matching value.
  • Bundle Drop uses a literal, per-platform runtime version by default. The same value is embedded in the native app and recorded on uploaded bundles. Exact equality is required during resolution.

This is the first question to settle during a migration: who decides that two builds are compatible, and how is that decision represented? A coarse boundary is easy to operate but admits more combinations. A precise boundary reduces the chance of a mismatch but usually creates more runtime lines and more release bookkeeping.

For Bundle Drop’s per-platform line model and authority choices, see Runtime versions: the compatibility boundary for React Native OTA updates. The configuration reference remains in Runtime Version.

Translate the operating model

CodePush concepts have close equivalents elsewhere, but the names do not always imply identical behavior. Treat the mapping as a starting point for migration, not proof of semantic parity.

CodePush conceptPurposeBundle Drop concept
App and deploymentScope updates and separate release tracksProject and channel
Deployment keySelect the deployment checked by a clientProject configuration and channel name
codePush.sync() or wrapperCheck, download, install, and reload according to policyBundleDrop.init(), hooks, and update policies
Target binary versionRestrict an update to compatible native versionsPer-platform runtime version
Rollout percentageExpose a release to part of a deploymentManaged publishing plus staged rollout
Install modeControl when a client installs an updateExplicit manual, on-next-launch, or immediate policy
Mandatory release flagRequire a client-facing update flowNo direct per-release equivalent; application policy and UI own the required behavior
Client/server rollbackRecover from a failing releaseDashboard rollback and on-device crash protection
Differential updateReduce transferred update contentPatch delivery with full-bundle fallback

CodePush supported differential delivery; migration planning should not erase that fact. The useful comparison is how each replacement creates, verifies, caches, and falls back from patches—not whether patches exist at all. Bundle Drop prepares optimized patches after upload and retains full-bundle delivery when a patch is unavailable or inefficient.

Compare the three paths

Expo Updates and EAS Update

Expo Updates is an open-source client implementation of the Expo Updates protocol. EAS Update is Expo’s hosted service around that client. Expo documents that the client can also point to a custom server implementing the protocol, so using expo-updates and buying the hosted EAS service are related but distinct decisions.

The model uses platforms and runtime versions at the protocol boundary, then adds channels and branches for hosted deployment workflows. EAS supports per-update and branch-based gradual releases; its current rollout documentation explains the constraints and commands for each. EAS rollback can republish a previous update or direct clients back to the embedded update, as described in the official rollback documentation.

This path is a natural candidate when Expo tooling already owns builds and deployment workflows, or when the team wants to stay inside one integrated release system. Evaluate the channel-to-branch model, runtime-version policy, rollout constraints, update protocol, and how the service fits existing observability and access-control requirements.

A managed React Native OTA platform

A managed provider owns the public update API, artifact storage, rollout state, operational dashboards, retention, and much of the delivery infrastructure. The application still needs a native client and an explicit runtime compatibility rule.

Bundle Drop is one such model. Channels are named release tracks, while managed publishing makes a rollout’s state part of server-side eligibility. Compatible rollouts are checked newest-first. A targeted or partial rollout that does not match can fall through to an older eligible rollout on the same channel and runtime line. Delivery can use a patch or a full bundle without changing the release decision.

The relevant evaluation is operational rather than rhetorical:

  • Does the resolve API fail safely when metadata or artifacts are unavailable?
  • Are rollout and targeting rules enforced by the server or trusted to client state?
  • Can teams see the exact bundle hash running on a device?
  • Are source maps tied to that same identity?
  • What happens when a patch cannot be produced, signed, downloaded, or applied?
  • Which controls are channel-scoped, project-scoped, or encoded into the binary?

Bundle Drop documents its answers under channels, rollback, and observability. Those pages are operational references; they are more useful during a proof of concept than a generic feature matrix.

Self-hosted update infrastructure

Self-hosting can mean running Microsoft’s standalone CodePush server, implementing the Expo Updates protocol, or building a proprietary resolve-and-delivery service. It preserves control over deployment topology, data locality, authentication, retention, and custom workflow rules.

It also makes the team responsible for the complete safety envelope:

  • compatible client and server protocols with reliable resolve and artifact delivery;
  • deterministic selection, integrity verification, rollback, and fallback behavior;
  • storage lifecycle, backups, disaster recovery, access control, and audit trails;
  • monitoring, incident response, upgrades, and capacity planning.

Self-hosting is sensible when these responsibilities align with existing platform engineering capabilities or regulatory constraints. It is rarely “free” simply because the source code is available. The relevant cost is sustained ownership: upgrades, on-call response, security work, capacity planning, and validation across React Native releases.

For a focused ownership comparison, see Managed vs Self-Hosted OTA: Which One Fits Your Team?.

Evaluate release control as a system

Release controls interact. A percentage rollout without a stable cohort can reshuffle users on each check. Targeting without a fallback rule can strand users on an unintended version. A rollback control that only changes server selection cannot help an offline device already trapped in a startup loop.

A useful review covers the main decision boundaries:

  1. Establish project, platform, channel, and runtime compatibility.
  2. Select an eligible release using documented publishing and audience rules.
  3. Deliver and verify the selected artifact independently of that eligibility decision.
  4. Stage and apply it according to client policy.
  5. Mark the candidate healthy or recover to a known-good bundle.

Bundle Drop’s exact newest-first, targeting, stable-bucket, and fallthrough semantics are described in How staged rollouts, targeting, and rollback fit together. The same exercise should be performed for any candidate platform, using production documentation and a test application rather than assumptions from similar terminology.

Make recovery observable

OTA recovery has two time horizons. Server-side rollback changes what future checks select. Device-side protection handles a bundle that has already been applied and fails during startup.

CodePush supported both server rollback and client rollback. Expo Updates includes update selection and error-recovery behavior, and EAS exposes hosted rollback workflows. Bundle Drop supports channel-scoped dashboard rollback plus a local health state for newly applied updates. These are not interchangeable implementations, even when they produce the same visible result.

Test recovery with the network disconnected, with no previous OTA bundle, and with a failure before the application becomes interactive. For Bundle Drop specifically, crash protection observes launches that reach the early BundleDrop.init() call. A native crash or a JavaScript failure before initialization is outside that mechanism.

Observability should retain a stable update identity from build through incident response. Record the running bundle hash alongside application version, platform, runtime version, and channel. Upload the matching source map under the same identity. Then a rollback decision can be based on a specific artifact rather than a vague correlation with an app-store version.

A migration sequence that preserves control

Avoid changing the provider and the release workflow at the same time. A staged migration is easier to diagnose:

  1. Inventory deployments, keys, target binary ranges, install modes, rollout practices, and rollback runbooks.
  2. Define the replacement runtime boundary for iOS and Android.
  3. Recreate release tracks as channels or the equivalent construct.
  4. Integrate the new client while keeping CodePush until the native paths are verified.
  5. Publish a harmless update to an internal channel and confirm identity, download, apply, restart, and source-map behavior.
  6. Exercise percentage rollout, audience exclusion, server rollback, and local startup failure.
  7. Build and ship store binaries with one OTA owner.
  8. Remove old deployment keys, native hooks, JavaScript wrappers, credentials, and CI steps.

The Bundle Drop CodePush migration guide covers the concrete integration sequence. The /codepush-alternative page provides a shorter product-level overview, but the migration should be governed by tested runtime behavior.

Choose based on ownership

The durable decision is not “which service looks most like CodePush?” It is which combination of client, service, and operating model fits the team.

Choose an Expo-centered path when its runtime and branch/channel workflow match how the app is already built and released. Choose a managed OTA provider when the team wants release controls and delivery operations without owning the service. Choose self-hosting when infrastructure control is a requirement and the organization is prepared to maintain the entire update path.

Whichever route you take, preserve the invariants CodePush made important: native compatibility must be explicit, release selection must be deterministic, the embedded bundle must remain a recovery point, and every running update must be identifiable in production.

Interested in safer OTA deployments?

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