react-native-nitro-ota
v0.12.0
Published
A package to react-native-nitro-ota
Readme
react-native-nitro-ota
Still in Alpha and will have issues
⚡️ High-performance Over-The-Air (OTA) updates for React Native - Powered by Nitro Modules
Download, unzip, and apply JavaScript bundle updates at runtime without going through the App Store or Play Store review process.
✨ Features
- 🚀 Native Performance - Built with Nitro Modules for maximum speed
- 🧵 Off JS Thread - All operations run on different threads, keeping your JS thread free
- 🌐 Server Agnostic - Works with any CDN, S3, GitHub Releases, or custom server
- 📦 Automatic Bundle Management - Handles download, extraction, and cleanup
- 🔒 Version Control - Built-in version checking and management
- 🛡️ Crash Safety - Auto-rollback if a new bundle crashes the app on first launch
- ↩️ Rollback - Manual rollback to the previous bundle with one call
- 🚫 Blacklisting - Bad versions are never re-downloaded
- 📊 Download Progress - Track download progress with a callback
📦 Installation
npm install react-native-nitro-ota react-native-nitro-modules
# or
yarn add react-native-nitro-ota react-native-nitro-modulesNote:
react-native-nitro-modulesis required as this library relies on Nitro Modules.
📱 Platform-Specific Setup (Required!)
Android: Native Bundle Loading
In your MainApplication.kt, add the bundle path loader:
import com.facebook.react.ReactApplication
import com.facebook.react.ReactNativeHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.margelo.nitro.nitroota.core.getStoredBundlePath
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages
override fun getJSMainModuleName(): String = "index"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
// 🔥 Load OTA bundle if available, otherwise use default
override fun getJSBundleFile(): String? {
return getStoredBundlePath(this@MainApplication)
}
}
}If using modern React host:
import com.facebook.react.ReactHost
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.margelo.nitro.nitroota.core.getStoredBundlePath
class MainApplication : Application(), ReactApplication {
override val reactHost: ReactHost by lazy {
getDefaultReactHost(
context = applicationContext,
packageList = PackageList(this).packages,
jsBundleFilePath = getStoredBundlePath(applicationContext)
)
}
}iOS: NitroOtaBundleManager
Install pods:
cd ios && pod installUpdate AppDelegate.swift:
import UIKit
import React
import NitroOtaBundleManager
class AppDelegate: UIResponder, UIApplicationDelegate {
override func bundleURL() -> URL? {
#if DEBUG
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
// Use OTA bundle if available, otherwise fall back to the bundled file
return NitroOtaBundleManager.shared.getStoredBundleURL()
?? Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}
}🚀 Quick Start
Option 1: GitHub OTA (Easiest! 🔥)
Use the githubOTA helper to point directly to a GitHub repository:
import { githubOTA, OTAUpdateManager } from 'react-native-nitro-ota';
// Configure GitHub URLs
const { downloadUrl, versionUrl } = githubOTA({
githubUrl: 'https://github.com/your-username/your-ota-repo',
otaVersionPath: 'ota.version', // or 'ota.version.json' for advanced features
ref: 'main', // optional, defaults to 'main'
});
// Create update manager
const otaManager = new OTAUpdateManager(downloadUrl, versionUrl);
// Check for updates
const hasUpdate = await otaManager.checkForUpdates();
if (hasUpdate) {
await otaManager.downloadUpdate();
otaManager.reloadApp();
}
// Or use advanced JS checking (supports JSON format)
const updateInfo = await otaManager.checkForUpdatesJS();
if (updateInfo?.hasUpdate && updateInfo.isCompatible) {
console.log('Compatible update available:', updateInfo.remoteVersion);
await otaManager.downloadUpdate();
otaManager.reloadApp();
}Option 2: Custom Server/CDN
import {
checkForOTAUpdates,
downloadZipFromUrl,
reloadApp,
} from 'react-native-nitro-ota';
const hasUpdate = await checkForOTAUpdates('https://your-cdn.com/ota.version');
if (hasUpdate) {
await downloadZipFromUrl('https://your-cdn.com/bundle.zip');
reloadApp();
}Custom Bundle File Path
By default the library auto-detects the bundle file inside the zip by scanning for .bundle (Android) or .jsbundle (iOS). If your zip uses a different file name or extension, pass the relative path as the third argument:
// Bundle is at the zip root with a .js extension
await downloadZipFromUrl(
'https://your-cdn.com/bundle.zip',
undefined, // no progress callback
'index.js'
);
// Bundle is inside a subfolder
await downloadZipFromUrl(
'https://your-cdn.com/bundle.zip',
(received, total) => console.log(`${received}/${total}`),
'build/main.bundle'
);
// Using OTAUpdateManager
const otaManager = new OTAUpdateManager(downloadUrl, versionUrl);
await otaManager.downloadUpdate(undefined, 'dist/index.js');When omitted (or undefined), the existing auto-detection logic is used — no changes needed for existing setups.
📊 Download Progress
Track download progress with an optional callback:
import { downloadZipFromUrl } from 'react-native-nitro-ota';
await downloadZipFromUrl(
'https://your-cdn.com/bundle.zip',
(received, total) => {
if (received === total) {
console.log(`Download complete: ${received} bytes`);
} else if (total > 0) {
const percent = Math.round((received / total) * 100);
console.log(`Downloading... ${percent}%`);
} else {
console.log(`Downloading... ${received} bytes`);
}
}
);Via OTAUpdateManager:
await otaManager.downloadUpdate((received, total) => {
setProgress(total > 0 ? received / total : -1);
});Note:
totalis-1during download when the server omitsContent-Length. The final callback always fires withreceived === total(the actual file size) to signal completion — use this instead of relying on the Promise resolve if you need the final byte count.
🛡️ Crash Safety & Rollback
How it works
The library uses a "pending confirmation" pattern to protect against bad bundles:
- A new bundle is downloaded →
ota_pending_validation = trueis stored - On the next app launch, the crash handler activates only if
pending_validation == true - You call
confirmBundle()after verifying your app works → guard is disabled - If the app crashes while unconfirmed → the crash handler automatically rolls back to the previous bundle, blacklists the bad version, and the next launch uses the restored bundle
Important: Crashes in confirmed bundles are completely unaffected — the crash handler passes through to your existing crash reporter (Crashlytics, Sentry, etc.).
1. Confirm a bundle after download
import {
downloadZipFromUrl,
confirmBundle,
reloadApp,
} from 'react-native-nitro-ota';
// After download, the bundle is "pending validation"
await downloadZipFromUrl(url);
reloadApp();
// On the new bundle: call confirmBundle() after verifying the app works
// (e.g. after a successful API call, a key screen loading, etc.)
confirmBundle();2. Manual rollback
import { rollbackToPreviousBundle, reloadApp } from 'react-native-nitro-ota';
const success = await rollbackToPreviousBundle();
if (success) {
reloadApp(); // restarts on the previous (or original) bundle
}3. Mark a bundle as bad manually
import { markCurrentBundleAsBad, reloadApp } from 'react-native-nitro-ota';
// Blacklists the current version and rolls back
await markCurrentBundleAsBad('payment_screen_broken');
reloadApp();4. Listen for rollback events
Subscribe to rollback events in your app root. The callback fires:
- Immediately if a crash rollback happened during the previous session (detected from persisted history)
- In the current session when
rollbackToPreviousBundle()ormarkCurrentBundleAsBad()succeeds
import { onRollback } from 'react-native-nitro-ota';
// Register early — e.g. at the top of your App component
const unsubscribe = onRollback((record) => {
console.log('Rollback happened!');
console.log(' From version:', record.fromVersion);
console.log(' To version: ', record.toVersion);
console.log(' Reason: ', record.reason);
console.log(' Timestamp: ', new Date(record.timestamp).toISOString());
// Send to your analytics or show a user-facing notice
});
// Call unsubscribe() when the component unmountsreason values:
| Value | Meaning |
|---|---|
| "crash_detected" | Crash handler auto-rolled back the bundle |
| "manual" | rollbackToPreviousBundle() was called |
| "max_rollbacks_exceeded" | Rollback counter > 3; reset to original bundle |
| custom string | Passed to markCurrentBundleAsBad(reason) |
5. Check rollback history
import { getRollbackHistory } from 'react-native-nitro-ota';
const history = await getRollbackHistory();
// [
// {
// timestamp: 1712345678000,
// fromVersion: "2",
// toVersion: "1",
// reason: "crash_detected"
// },
// ...
// ]6. Check & clear the blacklist
import { getBlacklistedVersions } from 'react-native-nitro-ota';
const blacklist = await getBlacklistedVersions();
console.log('Blacklisted versions:', blacklist); // ["2", "3"]Blacklisted versions are automatically skipped by checkForOTAUpdates() — they will never be downloaded again.
Rollback limits
| Consecutive rollbacks | Behaviour |
| --------------------- | ---------------------------------------------------------------- |
| 1–3 | Previous bundle is restored |
| > 3 | All OTA data cleared; app falls back to the original .jsbundle |
The counter resets to 0 whenever a new bundle is successfully downloaded.
Using OTAUpdateManager (class API)
All rollback features are also available on the class:
const otaManager = new OTAUpdateManager(downloadUrl, versionUrl);
// Listen for rollbacks
const unsub = otaManager.onRollback((record) => {
console.log('Rollback:', record.reason);
});
// Confirm bundle is working
otaManager.confirm();
// Manual rollback
const ok = await otaManager.rollback();
if (ok) otaManager.reloadApp();
// Mark as bad with a custom reason
await otaManager.markAsBad('checkout_screen_crash');
otaManager.reloadApp();
// Inspect history and blacklist
const history = await otaManager.getHistory();
const blacklist = await otaManager.getBlacklist();🔄 Background Updates (Experimental)
⚠️ HIGHLY ALPHA FEATURE - This feature is experimental and needs thorough testing. Use with caution in production.
Schedule automatic background checks for updates that run periodically:
import { OTAUpdateManager } from 'react-native-nitro-ota';
const otaManager = new OTAUpdateManager(downloadUrl, versionCheckUrl);
// Schedule background check every hour (3600 seconds)
otaManager.scheduleBackgroundCheck(3600);Note: Android uses WorkManager (minimum 15-minute interval). iOS uses background tasks (behavior depends on iOS version and system conditions).
📝 Understanding Version Files
Basic: ota.version (Simple Text)
The ota.version file is a simple text file that contains your current bundle version. The version can be anything - numbers, strings, or creative identifiers like "apple", "winter2024", "bugfix-v3".
echo "1.0.0" > ota.versionAdvanced: ota.version.json (With Metadata)
For more control, use the JSON format with semantic versioning and target app versions:
{
"version": "1.2.3",
"isSemver": true,
"targetVersions": {
"android": ["2.30.1", "2.30.2"],
"ios": ["2.30.1"]
},
"releaseNotes": "Bug fixes and improvements"
}JavaScript API for Advanced Checking:
import { checkForOTAUpdatesJS } from 'react-native-nitro-ota';
const result = await checkForOTAUpdatesJS(
'https://example.com/ota.version.json'
);
if (result?.hasUpdate && result.isCompatible) {
console.log(`New version: ${result.remoteVersion}`);
console.log(`Notes: ${result.metadata?.releaseNotes}`);
}Note: Both formats are supported. The library automatically detects which one you're using.
📦 Creating and Uploading Bundles
1. Generate the JavaScript Bundle
For Android
npx react-native bundle \
--platform android \
--dev false \
--entry-file index.js \
--bundle-output android/App-Bundles/index.android.bundle \
--assets-dest android/App-BundlesFor iOS
npx react-native bundle \
--platform ios \
--dev false \
--entry-file index.js \
--bundle-output ios/App-Bundles/index.jsbundle \
--assets-dest ios/App-Bundles2. Package the Bundle
# For Android
cd android && zip -r App-Bundles.zip App-Bundles
# For iOS
cd ios && zip -r App-Bundles.zip App-Bundles3. Distribute the Bundle
Upload the zipped bundle to your CDN, S3 bucket, GitHub Releases, or any file host.
🔑 Real-World Example
In the Jellify App:
- Bundles are uploaded to a dedicated Git branch named by version and platform (e.g.,
nitro_0.19.2_android). - The upload and versioning are automated via GitHub Actions workflow.
📚 API Reference
Functions
| Function | Description |
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| checkForOTAUpdates(url) | Returns true if a new version is available |
| downloadZipFromUrl(url, onProgress?, bundleFilePath?) | Downloads and unzips the bundle. Optional progress callback (received, total) => void. Optional bundleFilePath is the relative path to the bundle file inside the zip (e.g. "index.js", "build/main.bundle"); when omitted, auto-detection is used |
| getStoredOtaVersion() | Returns the currently active OTA version string, or null |
| getStoredUnzippedPath() | Returns the path to the active bundle file, or null |
| reloadApp() | Restarts the app to apply a downloaded bundle |
| confirmBundle() | Marks the current bundle as verified — disables crash guard |
| rollbackToPreviousBundle() | Rolls back to previous bundle; returns true on success |
| markCurrentBundleAsBad(reason) | Blacklists current bundle and triggers rollback |
| getBlacklistedVersions() | Returns string[] of blacklisted OTA versions |
| getRollbackHistory() | Returns RollbackHistoryRecord[] |
| onRollback(callback) | Subscribes to rollback events; returns an unsubscribe function |
| checkForOTAUpdatesJS(url?, appVersion?) | JS-side version check with detailed result |
| hasOTAUpdate(url?, appVersion?) | Simplified compatible-update check |
OTAUpdateManager class
| Method | Description |
| ---------------------------------------------- | ------------------------------------------------------ |
| checkForUpdates() | Native version check |
| checkForUpdatesJS(appVersion?) | JS-side version check |
| hasCompatibleUpdate(appVersion?) | Simple compatible-update check |
| downloadUpdate(onProgress?, bundleFilePath?) | Download with optional progress and custom bundle path |
| getVersion() | Current OTA version |
| getUnzippedPath() | Path to active bundle |
| reloadApp() | Restart the app |
| confirm() | Confirm bundle is working |
| rollback() | Roll back to previous bundle |
| markAsBad(reason?) | Blacklist + rollback with custom reason |
| getBlacklist() | List of blacklisted versions |
| getHistory() | Full rollback history |
| onRollback(callback) | Subscribe to rollback events |
| scheduleBackgroundCheck(interval) | Schedule periodic native background check |
RollbackHistoryRecord
interface RollbackHistoryRecord {
timestamp: number; // Unix ms
fromVersion: string; // OTA version that was active
toVersion: string; // Version restored ("original" = no OTA)
reason: 'crash_detected' | 'manual' | 'max_rollbacks_exceeded' | string; // custom reason from markCurrentBundleAsBad()
}🤝 Contributing
See CONTRIBUTING.md for development workflow and guidelines.
📄 License
MIT
