React Native · OTA Guide · 2026

React Native OTA Updates The complete 2026 guide.

How over-the-air updates actually work in React Native, the bundle lifecycle, what changed with the New Architecture, and how to choose the right platform after CodePush.

CI/CD Pipeline
$ npm run deploy:hotfix
Bundling app... Done
Size: 142 KB (Patch)
Pushed v2.4.1

Native Store Release

Pending Review

Avg Wait: 2-7 Days

No App Store Wait
Patch Updates
Global CDN Delivery
RN 0.70+
9:41
Build v2.4.1

Downloading bundle

760

KB

Patch update · 7.2× smaller

Verified · SHA-256
Signature OK
Applying...
Reload runtime
Apply Update

Six things to know upfront

OTA Ships JavaScript and Assets, Not Native Code

Anything that requires a `pod install` or Gradle change still needs an App Store release.

Microsoft App Center CodePush Retired on March 31, 2025

Every team running on hosted CodePush has had to migrate by now.

The Bundle Lifecycle Has Five Real Stages

These stages are build, upload, CDN, device check, and apply. Knowing each one is how you debug production failures.

The New Architecture (Bridgeless) Changed How Reload Works

If you're on RN 0.76+ and your OTA succeeds but doesn't reload, you're hitting this issue.

Patch Updates Beat Full Bundles by 10–40x

This saves on bandwidth, but only if your platform actually supports diffing.

App Store Guideline 3.3.2 explicitly permits OTA

This permission is for JavaScript and assets. The "Apple doesn't allow OTA" myth has been wrong for a decade.

The Foundation

What OTA actually means in a React Native app

Over-the-air updates let you change the JavaScript bundle and assets running on a user's device without going through the App Store or Play Store review process. That's the entire idea, and almost every interesting question about OTA comes down to what counts as "JavaScript and assets."

The native binary is a runtime.

Objective-C, Swift, Kotlin, Java, and all your linked native modules are compiled into a binary, signed, and shipped through Apple or Google. They are frozen the moment the build leaves CI.

Changing even one line of native code requires a new binary, a new submission, and a 24-hour to 7-day review window.

Your JavaScript bundle is the program that runs on it.

Metro takes all your JavaScript and produces main.jsbundle on iOS and index.android.bundle on Android. This payload is what OTA replaces at runtime.

The native shell has no idea this is happening. From its perspective, it's still loading a bundle from disk and handing it to Hermes.

The Boundary

The line between JS and native

Almost every OTA bug traces back to confusion about this boundary. Let's draw it precisely.

OTA-Updatable
Lives on the JavaScript side
React component tree, hooks, screens, navigation logic
Business logic, API client code, state management
JavaScript-only libraries — date formatting, validation, GraphQL clients
Static assets — images via require(), fonts, JSON files
Style declarations, theme variables, layout
Binary Release Required
Lives on the native side
Anything in Podfile or build.gradle
Native modules — Swift, Kotlin, Objective-C, Java code
The React Native runtime version itself (0.79 → 0.80 = release)
Info.plist, manifest permissions, app icons, launch screens
JavaScript engine choice (Hermes vs JSC) — set at build time
The version-skew trap
If you bump a native module's JavaScript side in your OTA bundle, the underlying binary on the user's device must support that version's native interface. Always pin native module versions to the binary release and only ship JavaScript-compatible patches through OTA.
The Lifecycle

The five stages of
an OTA deployment

Knowing what happens at each one is the difference between deploying confidently and shipping mystery failures.

Stage 01
Build

Metro produces main.jsbundle and assets. With Hermes, the bundle becomes pre-compiled bytecode.

Stage 02
Upload

CLI uploads to your OTA platform with metadata: target binary version, deployment key, release notes, optional rollout percentage.

Stage 03
Distribute

Platform pushes the bundle to a CDN. HTTP/3 and edge caching minimize time-to-first-byte from the user's location.

Stage 04
Check

On launch or foreground, the SDK queries the platform: "I'm version X, deployment Y, current hash Z. Anything newer?"

Stage 05
Apply

SDK downloads, verifies hash and signature, writes to disk, and either reloads immediately or waits for next launch.

A smarter alternative to Store updates

Patch updates beat full bundles by an order of magnitude. End-to-end deployment in seconds, not days.

~15s
End-to-end
Smaller patches
No
Store review
The Bundle

What's actually in the bundle

The output of npx react-native bundle is a single JavaScript file containing your entire app: every screen, every utility, and every npm dependency, all transformed and minified.

BUNDLE SIZE BY CATEGORY

Across ~40 production apps

Simple utility app 1.8 MB
Typical SaaS / fintech 3.2 MB
E-commerce with rich UI 5.4 MB
Social / feed-heavy app 8.1 MB
SAME RELEASE · THREE STRATEGIES

Patch updates vs full bundles

Full bundle (legacy) 5.4 MB
Diff patch (modern) 760 KB
Asset-only change 160 KB
The Policy

Apple does allow OTA
The myth is wrong

Every six months someone posts on Hacker News claiming Apple banned OTA updates. They didn't. The actual policy has been stable for nearly a decade.

App Store Review Guideline 3.3.2

Permits applications to download and execute interpreted code (JavaScript falls into this category) provided the downloaded code:

Doesn't change the app's primary purpose

Doesn't provide a store or storefront for additional features

Doesn't bypass App Store review for material new functionality

The Implementation

Drop-in code
No re-architecture

The code that ties this together is recognizable to anyone who has worked with CodePush because the AppsOnAir SDK is API-compatible by design.

App.tsx
_ release.sh
.github/workflows
Copy
import React, { useEffect, useState } from 'react';
import CodePush from '@code-push-next/react-native-code-push';

const App = () => { 
  CodePush.sync(
    {
      updateDialog: true,
      installMode: CodePush.InstallMode.IMMEDIATE,
      mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,
    },
    status => {
      const statusName = SyncStatusNameMap[status];
      setStatusCode(statusName);

      if (status === CodePush.SyncStatus.DOWNLOADING_PACKAGE) {
        setData('startTime', new Date().toTimeString());
      }
      if (status === CodePush.SyncStatus.UPDATE_INSTALLED) {
        setData('endTime', new Date().toTimeString());
      }
    },
  );
};

export default App;
#!/bin/bash
# Release script for OTA updates

echo "Starting release process..."
appsonair release-react \
  --app "MyAwesomeApp-iOS" \
  --deploymentName "Production" \
  --description "Performance improvements" \
  --mandatory true \
  --t 1.0.0

echo "Release deployed successfully!"
name: CodePush

on:
  push:
    branches:
      - main

jobs:
  codepush:
    runs-on: ubuntu-latest

    env:
      APPS_ON_AIR_TOKEN: ${{ secrets.APPS_ON_AIR_TOKEN }}
      APP_NAME_ANDROID: ${{ secrets.APP_NAME_ANDROID }}
      APP_NAME_IOS: ${{ secrets.APP_NAME_IOS }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "18" # Or your preferred version

      - name: Install AppsOnAir CLI
        run: npm install -g @appsonair/codepush-cli

      - name: Authenticate with AppsOnAir
        run: appsonair-codepush login --accessKey $APPS_ON_AIR_TOKEN

      - name: Install project dependencies
        run: npm install

      - name: Deploy CodePush - iOS
        run: appsonair-codepush release-react $APP_NAME_IOS

      - name: Deploy CodePush - Android
        run: appsonair-codepush release-react $APP_NAME_ANDROID

The --rollout 25 flag is the single most important habit. Release to 25% of users, watch error rates for an hour, then bump to 100%, or roll back instantly if anything goes sideways.

The Decision

The four real options
in 2026

With Microsoft App Center retired, every team has had to migrate. Here's an honest comparison of where teams are landing.

Capability
AppsOnAir CodePush
Self-hosted CP Server
Expo EAS Update
Newer entrants
Migration from App Center
checked
Drop-in, <1hr
Drop-in (same SDK)
Requires Expo SDK
Varies
React Native New Architecture
checked
Supported 0.70+
Community patches
Supported
Supported
Patch updates (diff bundles)
checked
Supported
Not natively
Supported
Supported
Bundle signing & SHA-256
checked
Built-in
Self-configure
Built-in
Varies
ISO 27001:2022
checked
Certified
Your responsibility
Certified
Rarely certified
Operational burden
checked
Low
High
Low
Low
Predictable pricing
checked
Flat tiers
Infra cost only
MAU + bandwidth
Often early-stage
Integrated stack (deep links + feedback)
checked
Full stack
Not included
Partial
Not included
Why Teams Choose

The drop-in replacement that doesn't ask you to relearn anything

Thousands of React Native teams had years of muscle memory built around the CodePush CLI, deployment-key model, and SDK API surface. Migrating away shouldn't mean throwing all of it out.

1:1 CLI mapping

Every appcenter codepush command has a direct AppsOnAir equivalent. Your CI scripts get a find-and-replace, not a rewrite.

New Architecture native

Tested across React Native 0.70 and above, including Bridgeless mode. Documented restart patterns for Bridgeless reload edge cases.

Patch updates included

Binary diffing on every release. Typical patches are 5–15% of the full bundle, cutting bandwidth by an order of magnitude.

Compliance-grade

SOC 2 Type II (in progress) and ISO 27001:2022. Audit-grade deployment logging. The compliance posture your enterprise customers will ask for.

Predictable pricing

Flat tiers without per-update or per-bandwidth-GB charges. Your bill doesn't compound when your release cadence increases.

Integrated stack

OTA distribution, deep linking (AppLink), in-app feedback (AppRemark), and force-update controls (AppSync) under one platform.

The Path

Migration in five steps

If you're coming from App Center CodePush, the path is the simplest of any option. Most teams complete it end-to-end in under a day.

01
Audit your current deployment

Note your binary versions in flight, deployment keys, current rollout state, and any custom SDK config.

02
Run both in parallel for 1–2 cycles

Existing CodePush deployments don't need to stop the day you start AppsOnAir. Run side-by-side and validate.

03
Swap the SDK package

Most apps need a single import change in App.tsx and a deployment-key update.

04
Update your CI scripts

Find-and-replace appcenter codepushappsonair-codepush. The argument shape is the same.

05
Verify with a canary rollout

Ship a no-op release to 5% of users, watch error rates and OTA telemetry, then ramp.

FAQ

Got Questions? We've Got Answers

Is Microsoft App Center CodePush still available in 2026?

Are React Native OTA updates allowed by Apple and Google?

Does AppsOnAir CodePush support React Native New Architecture and Bridgeless mode?

What's the difference between full bundle updates and patch updates?

Can I use OTA updates with Hermes?

How fast is an OTA update from upload to user device?