integrationReact NativeExpoiOSAndroidSDK

How to Integrate NitroPush: Native Wiring + JS SDK Guide

A step-by-step walkthrough of integrating NitroPush into your React Native or Expo app — from account setup to your first live OTA update. Covers iOS AppDelegate, Android MainApplication, and the full JS API surface.

N
NitroPush Team · May 30, 2026 · 12 min read

This guide walks through every step of wiring NitroPush into a React Native or Expo app — from creating your first project to shipping a live update to real devices. We’ll cover the native layer (Swift/Kotlin), the JS API, signing keys, and the CLI.

Prerequisites

  • A React Native ≥ 0.71 or Expo SDK ≥ 49 project
  • Node 18+, Xcode 14+ (iOS), Android Studio Flamingo+ (Android)
  • A NitroPush account — sign up free

Step 1 — Install the SDK

npm install @nitropush/react-native
# or
yarn add @nitropush/react-native

For Expo managed workflow, also add the config plugin (see Step 3b).


Step 2 — Create a project and environment

Install the CLI globally (or use npx):

npm install -g nitropush
nitropush login        # opens browser OAuth
nitropush whoami       # confirm you're authenticated

Create your project and a production environment:

nitropush app create --name "My App"
# → App ID: app_xxxxxxxxxxxx

nitropush env create --app app_xxxxxxxxxxxx --name prod
# → Deployment key: nl_prod_xxxxxxxxxxxxxxxx  ← copy this now

The deployment key is printed once at env create time. Copy it — you’ll need it in the native layer.


Step 3a — Native wiring: bare React Native

iOS — AppDelegate.swift

Open ios/<AppName>/AppDelegate.swift and make two changes:

import UIKit
import NitroPushSDK   // ← add this import

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

  var window: UIWindow?

  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    // NitroPush: configure with your deployment key
    NitroPushSdk.shared.configure(NPConfig(
      deploymentKey: "nl_prod_xxxxxxxxxxxxxxxx",  // ← your key here
      serverUrl: "https://api.nitropush.org",
      storageBaseUrl: "https://cdn.nitropush.org"
    ))
    return true
  }

  // NitroPush: serve the active OTA bundle in release builds
  func application(
    _ application: UIApplication,
    open url: URL,
    options: [UIApplication.OpenURLOptionsKey: Any] = [:]
  ) -> Bool { true }

  #if !DEBUG
  func bundleURL() -> URL? {
    return NitroPushSdk.shared.activeBundleURL() ?? Bundle.main.url(
      forResource: "main", withExtension: "jsbundle"
    )
  }
  #endif
}

Important: The #if !DEBUG guard ensures Metro still runs in development. Never remove it.

Add the deployment key to Info.plist for better key management:

<key>NitroPushDeploymentKey</key>
<string>nl_prod_xxxxxxxxxxxxxxxx</string>

Then read it in AppDelegate.swift:

let key = Bundle.main.infoDictionary?["NitroPushDeploymentKey"] as? String ?? ""
NitroPushSdk.shared.configure(NPConfig(deploymentKey: key, ...))

Run pod install after these changes.

Android — MainApplication.kt

Open android/app/src/main/java/<package>/MainApplication.kt:

import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactNativeHost
import com.nitropush.sdk.NitroPushSdk      // ← add

class MainApplication : Application(), ReactApplication {

  override fun onCreate() {
    super.onCreate()
    // NitroPush: install before ReactNativeHost initialises
    NitroPushSdk.install(this)             // ← add
  }

  override val reactNativeHost: ReactNativeHost =
    object : DefaultReactNativeHost(this) {

      // NitroPush: serve the active bundle in release builds
      override fun getJSBundleFile(): String? {
        return if (BuildConfig.DEBUG) null
        else NitroPushSdk.getInstance().jsBundleFilePath ?: super.getJSBundleFile()
      }

      override fun getPackages() = PackageList(this).packages
      override fun getJSMainModuleName() = "index"
      override fun getUseDeveloperSupport() = BuildConfig.DEBUG
    }
}

Add the deployment key to AndroidManifest.xml (inside <application>):

<meta-data
  android:name="NITROPUSH_DEPLOYMENT_KEY"
  android:value="nl_prod_xxxxxxxxxxxxxxxx" />

Step 3b — Native wiring: Expo managed workflow

Add the config plugin to app.json:

{
  "expo": {
    "plugins": [
      [
        "@nitropush/react-native",
        {
          "deploymentKey": "nl_prod_xxxxxxxxxxxxxxxx",
          "ios": true,
          "android": true
        }
      ]
    ]
  }
}

Then run prebuild to inject the native changes:

expo prebuild --clean

This writes the AppDelegate.swift and MainApplication.kt changes automatically via the config plugin. You don’t need to edit native files by hand.

Rebuild your dev client after prebuild:

eas build --profile development --platform all

Step 4 — JS API wiring

Open your app’s entry file (typically index.js or App.tsx) and add the configure() call at module scope — not inside a component:

import { configure, notifyAppReady, sync } from "@nitropush/react-native";

// Must be at module scope, before runApp/AppRegistry
configure();

// Rest of your app...
AppRegistry.registerComponent(appName, () => App);

configure() takes no arguments — the deployment key and server URLs are read from the native layer, where they’re set at compile time.

notifyAppReady

Call notifyAppReady() in your root component’s mount effect. This tells the SDK the update installed successfully. If it’s never called, the SDK rolls back to the previous bundle on next launch (the rollback safety net).

import { useEffect } from "react";
import { notifyAppReady } from "@nitropush/react-native";

export default function App() {
  useEffect(() => {
    // Confirm the update succeeded — clears the rollback timer
    notifyAppReady();
  }, []);

  return <YourApp />;
}

Checking for updates (sync)

Call sync() wherever you want to check for updates — typically when the app foregrounds:

import { sync, SyncStatus } from "@nitropush/react-native";
import { AppState } from "react-native";

useEffect(() => {
  const sub = AppState.addEventListener("change", async (state) => {
    if (state === "active") {
      const status = await sync({
        installMode: "ON_NEXT_RESTART",
        onProgress: (p) => console.log(`Downloading: ${p.receivedBytes}/${p.totalBytes}`),
      });

      if (status === SyncStatus.UPDATE_INSTALLED) {
        console.log("Update installed — will apply on next restart");
      }
    }
  });
  return () => sub.remove();
}, []);

Install modes:

  • ON_NEXT_RESTART — download now, apply on next cold launch (recommended)
  • IMMEDIATE — restart the JS bundle right after download (use for critical hotfixes only)
  • ON_NEXT_RESUME — apply when the app comes back from background

Bundle signing lets every device verify that each update came from you. An intercepted or tampered bundle is rejected before it touches the runtime.

Generate an ECDSA P-256 keypair:

nitropush app signing-key generate \
  --app app_xxxxxxxxxxxx \
  --out ./nitropush-signing.pem

This writes the private key to ./nitropush-signing.pem and registers the public key with NitroPush.

Immediately add to .gitignore:

nitropush-signing.pem

For CI, store the PEM content as a repository secret (NITROPUSH_SIGNING_KEY) and write it to a temp file at upload time:

KEY=$(mktemp)
printf '%s' "$NITROPUSH_SIGNING_KEY" > "$KEY"
nitropush release upload \
  --project app_xxxxxxxxxxxx \
  --environment prod \
  --app-version 1.0.0 \
  --label 1.0.1 \
  --bundle-path ./dist \
  --signing-key "$KEY"
rm -f "$KEY"

Step 6 — Ship your first update

Build a release bundle:

# React Native CLI
npx react-native bundle \
  --platform ios \
  --dev false \
  --entry-file index.js \
  --bundle-output ./dist/main.jsbundle \
  --assets-dest ./dist/assets

# Expo
npx expo export --platform all

Upload with the CLI:

nitropush release upload \
  --project app_xxxxxxxxxxxx \
  --environment prod \
  --app-version "1.0.0" \
  --label "1.0.1" \
  --bundle-path ./dist

The CLI outputs the release ID and a rollout status. By default it rolls out to 100% of devices on that project/environment/appVersion combination.


Step 7 — Verify the update on device

  1. Make a visible change in your JS (e.g. change a button label)
  2. Build and upload a new bundle
  3. Open the app on a device running the previous version
  4. Background and foreground the app (triggers the AppState sync)
  5. Kill and relaunch — the new bundle loads

The dashboard at app.nitropush.org shows install events, MAU, and rollout progress in real time.


Rollback safety net

The rollback is automatic. If you upload a broken bundle and it installs on device:

  1. The app crashes before notifyAppReady() fires
  2. On next launch, the SDK detects the unconfirmed state
  3. It boots the previous bundle automatically
  4. The failed release is marked in the dashboard

You never need to manually push a rollback — the SDK handles it at the native layer before JS even starts.


Summary: the minimal integration checklist

StepWhatWhere
1Install SDKnpm install @nitropush/react-native
2Create project + env + deployment keyCLI / dashboard
3Inject native bundle URL overrideAppDelegate / MainApplication
4configure() at module scopeindex.js
5notifyAppReady() on mountRoot component useEffect
6sync() on foregroundAppState listener
7Upload bundlenitropush release upload

That’s all it takes. Five minutes of setup, and every future JS fix ships to all your users before they’ve even closed the app.


Have questions? Reach us at contact@nitropush.org or open an issue on GitHub.