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.
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
Step 5 — Bundle signing (recommended for production)
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
- Make a visible change in your JS (e.g. change a button label)
- Build and upload a new bundle
- Open the app on a device running the previous version
- Background and foreground the app (triggers the
AppStatesync) - 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:
- The app crashes before
notifyAppReady()fires - On next launch, the SDK detects the unconfirmed state
- It boots the previous bundle automatically
- 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
| Step | What | Where |
|---|---|---|
| 1 | Install SDK | npm install @nitropush/react-native |
| 2 | Create project + env + deployment key | CLI / dashboard |
| 3 | Inject native bundle URL override | AppDelegate / MainApplication |
| 4 | configure() at module scope | index.js |
| 5 | notifyAppReady() on mount | Root component useEffect |
| 6 | sync() on foreground | AppState listener |
| 7 | Upload bundle | nitropush 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.