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+).
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 development 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.
The deployment workflow follows a strict promotion model:
- Generate the production bundle and upload it to a staging bucket via CLI.
- Verify the artifact in the management console.
- Promote the bundle to production with a defined rollout percentage.
- Monitor crash rates and performance metrics.
- 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
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Bare React Native, small team | Managed OTA (Stallion) | Zero infrastructure overhead, phased rollouts, signed bundles | Low (managed SaaS) |
| Expo-managed project | Expo EAS Update | Native framework integration, unified tooling | Medium (Expo tier pricing) |
| Strict compliance, air-gapped environment | Self-hosted CodePush | Full control over infrastructure, no external dependencies | High (infra, security, on-call) |
| High-traffic app, frequent JS patches | Differential OTA + Phased Rollouts | Bandwidth efficiency, rapid recovery, granular risk control | Low (managed SaaS) |
| Native-heavy updates, infrequent JS changes | Standard App Store Releases | OTA provides minimal value, store review is acceptable | Low (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
- Install SDK & CLI: Run
yarn add react-native-stallion and npm i -g @stallion/cli. Execute npx pod-install for iOS.
- Wire Native Resolution: Override
getJSBundleFile (Android) and bundleURL (iOS) to route release builds through the SDK while preserving Metro for development.
- Inject Credentials: Add
StallionProjectId and StallionAppToken to Info.plist and strings.xml. Generate tokens from the management console.
- Wrap Root Component: Apply the SDK's higher-order component to your app root and implement a custom hook for update state management.
- 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.