npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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-modules

Note: react-native-nitro-modules is 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 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
    }
}

🚀 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: total is -1 during download when the server omits Content-Length. The final callback always fires with received === 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:

  1. A new bundle is downloaded → ota_pending_validation = true is stored
  2. On the next app launch, the crash handler activates only if pending_validation == true
  3. You call confirmBundle() after verifying your app works → guard is disabled
  4. 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() or markCurrentBundleAsBad() 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 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) |

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.version

Advanced: 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-Bundles

For iOS

npx react-native bundle \
  --platform ios \
  --dev false \
  --entry-file index.js \
  --bundle-output ios/App-Bundles/index.jsbundle \
  --assets-dest ios/App-Bundles

2. Package the Bundle

# For Android
cd android && zip -r App-Bundles.zip App-Bundles

# For iOS
cd ios && zip -r App-Bundles.zip App-Bundles

3. 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:

📚 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