Back to KB
Difficulty
Intermediate
Read Time
8 min

Shipping React Native Updates Without the App Store: A Practical Guide to OTA and React Native Stallion

By Codcompass Team··8 min read

JavaScript-Only Hotfixes in React Native: Architecture, Implementation, and Operational Control

Current Situation Analysis

React Native applications operate on a dual-layer architecture. The native shell (APK or IPA) contains compiled Java, Kotlin, Objective-C, or Swift code, along with platform-specific permissions, icons, and splash screens. The JavaScript bundle is a separate artifact that the shell loads at runtime. App store review processes strictly govern the native shell. The JavaScript bundle, however, is treated as dynamic content.

This architectural split creates a persistent operational friction. When a production defect exists purely in the JavaScript layer—a typo, a misconfigured flag, a logic error—teams are forced to submit a full native binary to Apple and Google. Review cycles routinely span 24 to 72 hours. For high-traffic applications, this latency translates directly into revenue loss, degraded user trust, and inflated support ticket volumes.

Over-the-air (OTA) update mechanisms were designed to bridge this gap. They allow teams to replace the JavaScript bundle on user devices without touching the native shell. Despite their utility, OTA is frequently misunderstood. Many engineering teams treat it as a replacement for native release cycles or attempt to use it to bypass store review for functional changes. Both approaches violate platform guidelines and introduce severe supply chain risks. Apple and Google explicitly permit JavaScript-only OTA for bug fixes and minor UI adjustments, but they prohibit using OTA to fundamentally alter app functionality post-review.

The operational landscape shifted significantly in 2024 when Microsoft announced the retirement of App Center and its CodePush service, with full shutdown scheduled for 2025. Teams that relied on the hosted CodePush infrastructure faced an immediate migration requirement. The available paths diverged into three categories: adopting Expo EAS Update (which requires full Expo framework alignment), self-hosting the open-source CodePush server (which introduces infrastructure scaling, security, and on-call responsibilities), or migrating to a managed alternative like React Native Stallion.

Stallion emerged as a direct response to the CodePush retirement. It introduces differential patching, cryptographically signed bundles, and granular rollout controls. For teams running bare React Native who require CodePush-style ergonomics without framework lock-in, Stallion represents the current baseline for production-grade JavaScript hotfixing.

WOW Moment: Key Findings

The operational impact of OTA is not measured in convenience alone. It is measured in payload efficiency, rollout precision, and recovery velocity. Traditional store releases, monolithic OTA deployments, and differential OTA systems produce drastically different operational profiles.

ApproachAverage Payload Size (Minor Fix)Rollout GranularityRecovery Time (Broken Deploy)Infrastructure Overhead
Traditional App Store Release20–50 MB0% / 100% (or staged 10-20%)24–72 hoursLow
Monolithic OTA (Full Bundle)~20 MB0–100% (manual)1–2 hoursMedium (self-hosted)
Differential OTA (Stallion)<0.5 MB1–100% (phased)<5 minutesLow (managed)

Differential patching computes file-level deltas between the current and target bundle. Instead of transmitting the entire JavaScript artifact, only the modified bytes are delivered. In practice, this reduces bandwidth consumption by up to 98% for minor patches. On constrained networks, this difference determines whether an update completes within seconds or fails entirely.

Phased rollout controls shift risk management from platform-dependent staged submissions to direct engineering control. Teams can deploy to 1% of the user base, monitor crash telemetry and performance metrics, and incrementally expand coverage. If a defect surfaces, recovery happens in minutes rather than days.

Bundle signing adds a critical supply chain layer. Every OTA artifact is cryptographically verified before application. Without signature validation, a compromised CDN or intercepted network request could inject malicious JavaScript into production applications. Signed bundles ensure that only artifacts generated by your CI pipeline are executed on user devices.

Core Solution

Implementing a production-ready OTA pipeline requires coordinating three components: a CLI for artifact generation and upload, a native SDK for bundle resolution and health monitoring, and a management console for rollout control. The following implementation uses React Native Stallion, structured for bare React Native projects (0.69+).

1. Install Dependencies and CLI Tooling

The SDK runs on the device. The CLI operates in CI or local development environments.

# Project dependency
yarn add react-native-stallion

# Global or CI tooling
npm install -g @stallion/cli

Link iOS native modules:

npx pod-install

2. Override Native Bundle Resolution

React Native determines which JavaScript file to execute during the native host initialization. You must intercept this resolution path and route it through the OTA SDK for release builds, while preserving Metro for development.

Android (Kotlin, RN 0.76+):

import com.stallion.StallionBridge

class MainReactHost : DefaultReactNativeHost(application) {
    override fun getJSBundleFile(): String? {
        return if (BuildConfig.DEBUG) {
            null // Metro handles resolution
        } else {
            StallionBridge.resolveBundlePath(applicationContext)
        }
    }
}

iOS (Swift, RN 0.76+):

import ReactNativeStallion

func resolveBundleEndpoint() -> URL? {
    #if DEBUG
    return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
    #else
    return StallionRouter.provideBundleURL()
    #endif
}

Architecture Rationale: Branching on DEBUG/Release is mandatory. Metro serves hot-reloaded bundles during development. Routing deve

lopment traffic through the OTA SDK causes unnecessary network requests, breaks source maps, and interferes with debugging tooling. Release builds must delegate bundle resolution to the SDK to enable background downloads and atomic swaps.

3. Inject Runtime Credentials

The SDK requires a project identifier and an authentication token to communicate with the management console. These values must be embedded in native configuration files, not JavaScript constants, to prevent exposure in bundle artifacts.

iOS (Info.plist):

<key>StallionProjectId</key>
<string>prj_live_8f3a9c</string>
<key>StallionAppToken</key>
<string>tok_prod_x7k2m9</string>

Android (res/values/strings.xml):

<string name="StallionProjectId">prj_live_8f3a9c</string>
<string name="StallionAppToken">tok_prod_x7k2m9</string>

4. Wrap the Application Root and Handle Update Lifecycle

The SDK provides a higher-order component that initializes the update checker and exposes lifecycle hooks. A custom hook abstracts the update state for UI integration.

import { withBundleRouter, useUpdateState, triggerRestart } from 'react-native-stallion';

const AppContainer = () => {
  const { pendingUpdate, isRestartMandatory, releaseNotes } = useUpdateState();

  if (isRestartMandatory && pendingUpdate) {
    return (
      <View style={styles.overlay}>
        <Text>{releaseNotes || 'An update is ready.'}</Text>
        <Button title="Apply Now" onPress={triggerRestart} />
      </View>
    );
  }

  return <MainApplication />;
};

export default withBundleRouter(AppContainer);

Architecture Rationale: The root wrapper ensures the SDK initializes before any business logic executes. The useUpdateState hook decouples update metadata from UI rendering, allowing teams to implement silent background downloads, mandatory restart banners, or deferred update prompts based on product requirements. The triggerRestart function performs an atomic bundle swap, ensuring the new JavaScript context replaces the old one without partial state corruption.

5. Publish and Promote Artifacts

The deployment workflow follows a strict promotion model:

  1. Generate the production bundle and upload it to a staging bucket via CLI.
  2. Verify the artifact in the management console.
  3. Promote the bundle to production with a defined rollout percentage.
  4. Monitor crash rates and performance metrics.
  5. Increment rollout or execute rollback if thresholds are breached.
stallion-cli publish \
  --platform ios \
  --bundle-path ./dist/main.jsbundle \
  --target-version 2.4.1 \
  --rollout 5

The --rollout flag defaults to 0, meaning only explicitly enrolled test devices receive the update. Incremental expansion (5% → 25% → 100%) aligns with standard canary deployment practices. The SDK automatically checks for updates on cold start and foreground transitions, applying patches in the background without interrupting user sessions.

Pitfall Guide

1. Attempting Native Module Swaps via OTA

Explanation: OTA only replaces the JavaScript bundle. Adding, removing, or updating native modules requires recompiling the native shell. Attempting to call a newly added native method from an OTA bundle will throw a runtime exception. Fix: Treat OTA as a JavaScript-only patching mechanism. Any change touching android/, ios/, or native dependencies must follow a standard app store release.

2. Bypassing Phased Rollouts

Explanation: Deploying to 100% of users immediately eliminates the safety net. If a bundle contains a silent logic error or performance regression, the entire user base is affected before telemetry can surface the issue. Fix: Always initialize releases at 1–5%. Use crash reporting (Sentry, Firebase Crashlytics) and APM tools to validate stability before expanding coverage.

3. Ignoring Health Check Markers

Explanation: The SDK relies on a health check signal to confirm a bundle is stable. If the application crashes before marking itself as healthy, the SDK assumes the bundle is defective and reverts on the next launch. Fix: Ensure your root component or initialization sequence calls the health confirmation method within the first 30 seconds of startup. Delayed health checks trigger false-positive rollbacks.

4. Shipping Unsigned Bundles in Production

Explanation: Unsigned artifacts are vulnerable to man-in-the-middle attacks and CDN compromise. An attacker could intercept the download request and inject malicious JavaScript. Fix: Enable bundle signing in the management console. Rotate signing keys quarterly. Verify signature validation is enabled in the SDK configuration before production deployment.

5. Overwriting Metro Dev Server in Release Builds

Explanation: Failing to branch on DEBUG/Release in native bundle resolution causes development builds to route through the OTA SDK. This breaks hot reloading, corrupts source maps, and generates unnecessary network traffic. Fix: Maintain explicit conditional logic in getJSBundleFile and bundleURL. Route development traffic to Metro and release traffic to the OTA SDK.

6. Treating OTA as a Feature Delivery Channel

Explanation: Using OTA to ship major features violates app store guidelines. Apple and Google permit OTA for bug fixes and minor adjustments, not for functional changes that alter the app's core purpose. Fix: Reserve OTA for patches, copy fixes, styling adjustments, and business logic tweaks. Major features must be bundled in native releases and submitted for review.

7. Neglecting Bundle Version Alignment

Explanation: Uploading a bundle targeting an app version that does not exist in the console, or mismatching semantic versions, causes the SDK to ignore the update or apply it to incompatible clients. Fix: Align bundle target versions with your native release cadence. Use CI/CD pipelines to automatically tag bundles with the correct --target-version flag based on package.json or app.json.

Production Bundle

Action Checklist

  • Verify React Native version is 0.69 or higher before SDK integration
  • Install react-native-stallion and link iOS native modules via npx pod-install
  • Implement conditional bundle resolution in Android and iOS native hosts
  • Inject project ID and app token into native configuration files
  • Wrap the root component with the SDK's higher-order component
  • Enable bundle signing and configure customer-managed keys if required
  • Integrate CLI publish commands into CI/CD pipelines with automated version tagging
  • Configure crash telemetry correlation with OTA rollout phases

Decision Matrix

ScenarioRecommended ApproachWhyCost Impact
Bare React Native, small teamManaged OTA (Stallion)Zero infrastructure overhead, phased rollouts, signed bundlesLow (managed SaaS)
Expo-managed projectExpo EAS UpdateNative framework integration, unified toolingMedium (Expo tier pricing)
Strict compliance, air-gapped environmentSelf-hosted CodePushFull control over infrastructure, no external dependenciesHigh (infra, security, on-call)
High-traffic app, frequent JS patchesDifferential OTA + Phased RolloutsBandwidth efficiency, rapid recovery, granular risk controlLow (managed SaaS)
Native-heavy updates, infrequent JS changesStandard App Store ReleasesOTA provides minimal value, store review is acceptableLow (no OTA cost)

Configuration Template

// stallion.config.ts
export const bundleRouterConfig = {
  projectId: process.env.STALLION_PROJECT_ID,
  appToken: process.env.STALLION_APP_TOKEN,
  healthCheckTimeout: 30000,
  autoRollbackOnCrash: true,
  signingVerification: 'strict',
  rolloutStrategy: {
    initialPercentage: 5,
    incrementStep: 25,
    maxRetries: 3,
    telemetryIntegration: 'sentry'
  }
};
# CI/CD Pipeline Snippet (GitHub Actions)
- name: Build JavaScript Bundle
  run: npx react-native bundle --entry-file index.js --platform ios --dev false --bundle-output ./dist/main.jsbundle

- name: Publish to OTA Console
  run: |
    stallion-cli publish \
      --platform ios \
      --bundle-path ./dist/main.jsbundle \
      --target-version ${{ github.ref_name }} \
      --rollout 5 \
      --release-notes "Patch: Fix navigation stack overflow"

Quick Start Guide

  1. Install SDK & CLI: Run yarn add react-native-stallion and npm i -g @stallion/cli. Execute npx pod-install for iOS.
  2. Wire Native Resolution: Override getJSBundleFile (Android) and bundleURL (iOS) to route release builds through the SDK while preserving Metro for development.
  3. Inject Credentials: Add StallionProjectId and StallionAppToken to Info.plist and strings.xml. Generate tokens from the management console.
  4. Wrap Root Component: Apply the SDK's higher-order component to your app root and implement a custom hook for update state management.
  5. Publish First Patch: Build the production bundle, upload via CLI with --rollout 5, and verify the update downloads and applies on a test device. Monitor crash telemetry before expanding rollout.