
Company News
Jerod Santo Joins Socket as Head of Media
Allow myself to introduce... myself.
react-native-nitro-ota
Advanced tools
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.
npm install react-native-nitro-ota react-native-nitro-modules
# or
yarn add react-native-nitro-ota react-native-nitro-modules
Note:
react-native-nitro-modulesis required as this library relies on Nitro Modules.
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)
)
}
}
Install pods:
cd ios && pod install
Update 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
}
}
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();
}
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();
}
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.
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.
The library uses a "pending confirmation" pattern to protect against bad bundles:
ota_pending_validation = true is storedpending_validation == trueconfirmBundle() after verifying your app works → guard is disabledImportant: Crashes in confirmed bundles are completely unaffected — the crash handler passes through to your existing crash reporter (Crashlytics, Sentry, etc.).
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();
import { rollbackToPreviousBundle, reloadApp } from 'react-native-nitro-ota';
const success = await rollbackToPreviousBundle();
if (success) {
reloadApp(); // restarts on the previous (or original) bundle
}
import { markCurrentBundleAsBad, reloadApp } from 'react-native-nitro-ota';
// Blacklists the current version and rolls back
await markCurrentBundleAsBad('payment_screen_broken');
reloadApp();
Subscribe to rollback events in your app root. The callback fires:
rollbackToPreviousBundle() or markCurrentBundleAsBad() succeedsimport { 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 unmounts
reason 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) |
import { getRollbackHistory } from 'react-native-nitro-ota';
const history = await getRollbackHistory();
// [
// {
// timestamp: 1712345678000,
// fromVersion: "2",
// toVersion: "1",
// reason: "crash_detected"
// },
// ...
// ]
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.
| 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.
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();
⚠️ 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).
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.version
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",
"patches": {
"1.2.2": "patches/1.2.2-1.2.3.zip"
}
}
patches is optional and written for you by npx nitro-ota patch — see Differential (Patch) Updates.
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.
npx react-native bundle \
--platform android \
--dev false \
--entry-file index.js \
--bundle-output android/App-Bundles/index.android.bundle \
--assets-dest android/App-Bundles
npx react-native bundle \
--platform ios \
--dev false \
--entry-file index.js \
--bundle-output ios/App-Bundles/index.jsbundle \
--assets-dest ios/App-Bundles
# For Android
cd android && zip -r App-Bundles.zip App-Bundles
# For iOS
cd ios && zip -r App-Bundles.zip App-Bundles
Upload the zipped bundle to your CDN, S3 bucket, GitHub Releases, or any file host.
In the Jellify App:
nitro_0.19.2_android).Most releases change a few kilobytes of JavaScript, yet every device downloads the whole zip. With patches, a device that already runs version N downloads a small patch zip and rebuilds version N+1 locally. Patches are generated once at publish time and served as plain files, so this works on any static host (GitHub, S3, CDN) — no server logic.
npx nitro-ota patch computes a bsdiff delta between the previous bundle and the new one and writes a patch zip: the same layout as your full zip, with <bundle> replaced by <bundle>.patch. Assets and ota.version.json are included as-is.
It rewrites ota.version.json next to the bundle:
{
"version": "1.2.3",
"patches": {
"1.2.2": "patches/1.2.2-1.2.3.zip"
}
}
Keys are the exact version strings devices have stored; values are URLs, absolute or relative to the manifest.
OTAUpdateManager.downloadUpdate() looks up the installed version in patches. When a patch exists it downloads that instead of the full zip and rebuilds the bundle on device, verifying the SHA-256 of both the base bundle and the result. Any failure (no installed bundle, hash mismatch, network error) falls back to the full download automatically.
Requirements: pass a versionCheckUrl that points at ota.version.json, and keep publishing the full zip — first installs and fallbacks use it. Everything is backward compatible: manifests without patches and plain ota.version files behave exactly as before, and older app versions ignore the field.
npx nitro-ota patch --old ./previous-release --new ./App-Bundles
| Option | Description |
|---|---|
--old <dir> | Previously published release directory. Repeat it to support several installed versions (one patch each) |
--new <dir> | New release directory (bundle, assets, ota.version.json) |
--out <dir> | Where to write patch zips (default: <new>/patches) |
--bundle <path> | Bundle path relative to the release directory (default: auto-detect *.bundle / *.jsbundle) |
--from <v> / --to <v> | Versions (default: read from each directory's ota.version.json or ota.version) |
--url-base <url> | Host patches elsewhere; manifest entries become <url-base>/<file> |
Each run replaces patches with exactly the patches it generated, so list every --old you want to support in one run. The CLI needs Node 22.2+ and has no dependencies. If you pass a custom bundleFilePath to downloadUpdate, use the same relative path as --bundle.
The previous release is the previous commit of your bundles branch. GitHub's archive zip (your full download) honours .gitattributes, so committed patches stay out of it while raw.githubusercontent.com still serves them:
# inside the checkout of your bundles branch, before writing the new bundle
git worktree add ../previous HEAD
npx react-native bundle --platform android --dev false --entry-file index.js \
--bundle-output index.android.bundle --assets-dest .
echo "{ \"version\": \"$NEW_VERSION\" }" > ota.version.json
rm -rf patches
npx nitro-ota patch --old ../previous --new .
echo 'patches/ export-ignore' > .gitattributes
git add -A && git commit -m "OTA $NEW_VERSION" && git push
onProgress restarts from 0 with the full size.scheduleBackgroundCheck() always downloads the full zip.| 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 |
findPatchUrl(manifestUrl, fromVersion) | Resolves the patch zip URL that upgrades fromVersion to the manifest's version, or null |
isPatchSupported() | true when the installed native library can apply patches; false on older binaries, where downloadUpdate() always uses the full zip |
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; downloads a patch when the manifest lists one for the installed version |
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 |
lastDownload | { url, patch, bytes } of the most recent downloadUpdate(), or null |
RollbackHistoryRecordinterface 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()
}
See CONTRIBUTING.md for development workflow and guidelines.
MIT
FAQs
A package to react-native-nitro-ota
The npm package react-native-nitro-ota receives a total of 259 weekly downloads. As such, react-native-nitro-ota popularity was classified as not popular.
We found that react-native-nitro-ota demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Company News
Allow myself to introduce... myself.

Research
/Security News
A Twitch browser extension on Chrome and Firefox forwards users’ live OAuth session tokens through proxies controlled by a Russian bot service.

Security News
Anthropic found biased reasoning and recklessness drove Claude Mythos 5 to publish malware on PyPI and compromise a security vendor.