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.
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
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 concept | Purpose | Bundle Drop concept |
|---|---|---|
| App and deployment | Scope updates and separate release tracks | Project and channel |
| Deployment key | Select the deployment checked by a client | Project configuration and channel name |
codePush.sync() or wrapper | Check, download, install, and reload according to policy | BundleDrop.init(), hooks, and update policies |
| Target binary version | Restrict an update to compatible native versions | Per-platform runtime version |
| Rollout percentage | Expose a release to part of a deployment | Managed publishing plus staged rollout |
| Install mode | Control when a client installs an update | Explicit manual, on-next-launch, or immediate policy |
| Mandatory release flag | Require a client-facing update flow | No direct per-release equivalent; application policy and UI own the required behavior |
| Client/server rollback | Recover from a failing release | Dashboard rollback and on-device crash protection |
| Differential update | Reduce transferred update content | Patch 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:
- Establish project, platform, channel, and runtime compatibility.
- Select an eligible release using documented publishing and audience rules.
- Deliver and verify the selected artifact independently of that eligibility decision.
- Stage and apply it according to client policy.
- 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:
- Inventory deployments, keys, target binary ranges, install modes, rollout practices, and rollback runbooks.
- Define the replacement runtime boundary for iOS and Android.
- Recreate release tracks as channels or the equivalent construct.
- Integrate the new client while keeping CodePush until the native paths are verified.
- Publish a harmless update to an internal channel and confirm identity, download, apply, restart, and source-map behavior.
- Exercise percentage rollout, audience exclusion, server rollback, and local startup failure.
- Build and ship store binaries with one OTA owner.
- 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.