Native-first OTA updates for React Native apps.
The package gives you two pieces:
- a native OTA runtime for iOS and Android
- a release CLI,
react-native-ota-release, that builds and publishes OTA bundles
The update flow is entirely native — there is no public JS API and no JS bootstrap call required.
On launch the native runtime picks the best available bundle (OTA or embedded) for React Native to load, then concurrently checks for a newer bundle by fetching a JSON manifest from your CDN. If the manifest has a strictly newer bundleVersion, the runtime downloads the zip, extracts it, reloads React Native, and marks the new bundle as pending.
A bundle stays pending until React renders content and a 3-second grace period passes — at that point it is confirmed. If the app crashes before confirmation, the next launch finds currentPending=true in persisted state and rolls back to the previous bundle automatically. A bad OTA can never brick the app: worst case is two silent crashes, then a clean rollback.
Bundle selection priority on startup:
- Confirmed current OTA bundle
- Previous OTA bundle (if current is missing or corrupted)
- Embedded bundle shipped with the binary
binaryVersion scoping: The manifest URL includes the native app version (CFBundleShortVersionString on iOS, versionName on Android). This scopes OTA updates to a specific binary — a bundle built for v1.2 is never applied to a v1.3 binary.
Run the installer — it installs the package, writes publicUrlBase to your package.json, patches the native startup files, and runs CocoaPods:
npx react-native-ota-release install \
--public-url-base https://cdn.example.com/mobile/prodSee install options for all flags.
Install the package yourself, configure publicUrlBase in package.json, and wire the native startup in AppDelegate.swift and MainApplication.kt.
Steps
1. Install the package
npm install @imcsorin/react-native-otaFor iOS, run pods after:
cd ios && pod install2. Set publicUrlBase in your app package.json
This is the only supported configuration surface. Do not add OTA config to Info.plist or AndroidManifest.xml.
{
"react-native-ota": {
"publicUrlBase": "https://cdn.example.com/mobile/prod"
}
}3. Wire the native startup
iOS — add the import and override bundleURL() in your RCTDefaultReactNativeFactoryDelegate subclass:
import ReactNativeOta
...
override func bundleURL() -> URL? {
#if DEBUG
RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
RNOtaManager.shared.bundleURL()
#endif
}bundleURL() restores persisted OTA state, starts the background update check, and returns the URL for React Native to load.
Android — add the import and override getJSBundleFile() inside your DefaultReactNativeHost. Pass the invalidateReactHost lambda so the controller can clear the cached ReactHost before reloading:
import com.imcsorin.reactnativeota.ReactNativeOtaController
...
override fun getJSBundleFile(): String? =
ReactNativeOtaController.getJSBundleFile(this@MainApplication) {
reactNativeHost.clear()
currentReactHost = null
}The manifest is a JSON file your CDN serves at <publicUrlBase>/manifests/<platform>/<binaryVersion>.json. The runtime only installs the bundle when bundleVersion is a non-negative integer strictly newer than whatever is currently running.
{
"bundleVersion": 1024,
"downloadUrl": "https://cdn.example.com/mobile/prod/updates/1024/ios.zip"
}Missing, malformed, or not-newer manifests are silently ignored.
Archive layout:
- iOS:
main.jsbundleat the zip root plus bundled assets - Android:
index.android.bundleat the zip root plus bundled assets
Run the release CLI from your React Native app root:
export AWS_ACCESS_KEY_ID=<aws-access-key-id>
export AWS_SECRET_ACCESS_KEY=<aws-secret-access-key>
export AWS_ENDPOINT=https://s3.example.com
export AWS_PATH=my-bucket/mobile/prod
npx react-native-ota-release publish \
--bundle-version 1024By default the CLI:
- builds release bundles for iOS and Android
- writes local artifacts to
.react-native-ota/<bundleVersion>/ - uploads
manifests/ios/<iosBinaryVersion>.json - uploads
manifests/android/<androidBinaryVersion>.json - uploads
updates/<bundleVersion>/ios.zip - uploads
updates/<bundleVersion>/android.zip
If you omit --bundle-version, the CLI generates a timestamp-derived integer.
The CLI reads react-native-ota.publicUrlBase from your app package.json to construct download URLs in the manifest.
npx react-native-ota-release install [options]| Flag | Description |
|---|---|
--project-root <path> |
React Native app root to patch. |
--platform <all|ios|android|ios,android> |
Patch one or both native platforms. Default is all. |
--public-url-base <url> |
Writes react-native-ota.publicUrlBase into your app package.json. |
--package-manager <npm|yarn|pnpm|bun> |
Override package manager auto-detection. |
--skip-package-install |
Only patch native files and package.json; do not run the package manager. |
--skip-pods |
Skip pod install after patching iOS. |
| Flag | Description |
|---|---|
--bundle-version <value> |
Explicit OTA bundle version as an integer. If omitted, a timestamp-derived integer is generated. |
--platform <all|ios|android|ios,android> |
Build one or both platforms. Default is all. |
--project-root <path> |
React Native app root to bundle from. |
--entry-file <path> |
Override the React Native entry file. |
--metro-config <path> |
Override the Metro config path. |
--ios-binary-version <value> |
Override detected iOS app version. |
--android-binary-version <value> |
Override detected Android app version. |
--output-dir <path> |
Write local artifacts somewhere other than .react-native-ota/. |
--dry-run |
Build artifacts and print the manifest/upload plan without uploading. |
| Variable | Required | Description |
|---|---|---|
AWS_ENDPOINT |
Yes | S3 or S3-compatible endpoint, such as https://s3.example.com or http://127.0.0.1:31337. |
AWS_PATH |
Yes | Upload destination in bucket/prefix form, such as my-bucket/mobile/prod. |
AWS_ACCESS_KEY_ID |
Yes | AWS access key. |
AWS_SECRET_ACCESS_KEY |
Yes | AWS secret key. |
The CLI fails fast when required environment variables are missing.
- Development setup
- Development workflow
- Local OTA server and E2E tests
- Sending a pull request
- Code of conduct
MIT