Installation

1. Install the package

yarn add @nitropush/react-native react-native-nitro-modules

react-native-nitro-modules is a peer dependency. If you already use it for other Nitro Modules, you can skip the second package.

2. Native wiring

Two pieces of native glue that have to live in your project regardless of whether you’ll drive the SDK from JS or natively:

  1. The launch-time pointer sweep — runs the rollback safety net before the JS bundle loads.
  2. The bundleURL / getJSBundleFile override — hands React the active OTA bundle when one’s available.

Wire it into AppDelegate.swift. Touching NitroPushSdk.shared for the first time runs the launch-time pointer sweep + lifecycle observers.

import NitroPush

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
  // …

  override func bundleURL() -> URL? {
  #if DEBUG
    return RCTBundleURLProvider.sharedSettings()
      .jsBundleURL(forBundleRoot: "index")
  #else
    // Hand React the active OTA bundle, falling back to the
    // binary-shipped one. The first time `NitroPushSdk.shared` is
    // touched, it runs `consumePendingPointerOnLaunch()` — which
    // activates the pending bundle (or rolls back if the previous
    // install was unhealthy).
    return NitroPushSdk.shared.activeBundleURL()
        ?? Bundle.main.url(forResource: "main", withExtension: "jsbundle")
  #endif
  }
}

Wire it into MainApplication.kt. NitroPushSdk.install(this) creates the singleton; activeBundleFile() goes into ReactNativeHost.

import com.nitropush.sdk.NitroPushSdk

class MainApplication : Application(), ReactApplication {
  override fun onCreate() {
    super.onCreate()
    // Touches the singleton — runs the launch-time pointer sweep
    // + lifecycle observers.
    NitroPushSdk.install(this)
  }

  override val reactNativeHost: ReactNativeHost =
    object : DefaultReactNativeHost(this) {
      override fun getJSBundleFile(): String? {
        if (BuildConfig.DEBUG) return null
        // Returning null in debug keeps Metro in charge.
        // In release, returns the active OTA bundle path or null
        // (in which case React falls back to the binary-shipped bundle).
        return NitroPushSdk.shared.activeBundleFile()
      }
      // …
    }
}

After this step, run:

Install the CocoaPods dependency.

cd ios && pod install

Gradle picks the package up from your existing React Native autolinking — no extra config.

# nothing — autolinking handles it

Xcode 26 — bare React Native only. Xcode 26’s strict modular headers validate the auto-generated CocoaPods umbrella in pure ObjC mode, where nitrogen’s C++ .hpp imports fail with unknown type name 'namespace'. Add the following post_install hook to your ios/Podfile so the umbrella’s .hpp imports get wrapped in #ifdef __cplusplus after pod install — ObjC validation then skips them while Swift’s C++ interop still picks them up:

post_install do |installer|
  # …existing react_native_post_install(installer, ...) call…

  umbrella = File.join(
    installer.sandbox.root.to_s,
    'Target Support Files/NitroPushNative/NitroPushNative-umbrella.h'
  )
  if File.exist?(umbrella)
    content = File.read(umbrella)
    hpp_imports = content.scan(/^#import\s+"[^"]+\.hpp"\s*$/).join("\n")
    unless hpp_imports.empty? || content.include?('#ifdef __cplusplus')
      guarded = "#ifdef __cplusplus\n#{hpp_imports}\n#endif"
      patched = content.gsub(/^#import\s+"[^"]+\.hpp"\s*\n/, '')
                       .sub(/(FOUNDATION_EXPORT double)/, "#{guarded}\n\n\\1")
      File.write(umbrella, patched)
    end
  end
end

Expo apps don’t need this — the config plugin injects the same hook at expo prebuild time. See apps/react-native-example/ios/Podfile for the full reference.

3. Configure environment variables

The SDK needs three values: serverUrl, deploymentKey, storageBaseUrl. Where they come from depends on whether you’ll call configure() from JS or natively.

VariableWhat
serverUrlNitroPush API base URL. Production: https://app.nitropush.org.
deploymentKeyThe environment’s deployment key (from Projects).
storageBaseUrlPublic root of your storage bucket. Visible on the Storage page once a provider is configured. With MinIO this is the S3 API port (typically :9000), not the Console UI port (typically :9001).

There are two equivalent ways to feed these to the SDK:

  • Native-side — put NITROPUSH_SERVER_URL, NITROPUSH_DEPLOYMENT_KEY, NITROPUSH_STORAGE_BASE_URL in Info.plist (iOS) and <application> <meta-data> in AndroidManifest.xml (Android), then call NitroPushSdk.configFromInfoPlist() / NitroPushSdk.configFromManifest(). From JS the no-arg configure() reads the same keys.
  • Explicit-config — pass an NPConfig / NlConfig / NitroPushConfig object yourself. Useful when values come from a build-time secret, a feature-flag service, or per-target xcconfig.

Add NITROPUSH_* keys to Info.plist and use configFromInfoPlist(), or pass an NPConfig you assembled yourself.

<!-- ios/<App>/Info.plist -->
<key>NITROPUSH_SERVER_URL</key>
<string>https://app.nitropush.org</string>
<key>NITROPUSH_DEPLOYMENT_KEY</key>
<string>PROD-KEY-…</string>
<key>NITROPUSH_STORAGE_BASE_URL</key>
<string>https://your-bucket.s3.amazonaws.com</string>
// Option A: read the keys above via the SDK helper.
try NitroPushSdk.shared.configure(NitroPushSdk.configFromInfoPlist())

// Option B: assemble your own NPConfig (e.g. from xcconfig + Bundle.main).
let key = Bundle.main.object(forInfoDictionaryKey: "NITROPUSH_DEPLOYMENT_KEY") as? String ?? ""
try NitroPushSdk.shared.configure(
  NPConfig(
    serverUrl:      "https://app.nitropush.org",
    deploymentKey:  key,
    storageBaseUrl: "https://your-bucket.s3.amazonaws.com"
  )
)

Add NITROPUSH_* under <application> <meta-data> in AndroidManifest.xml and use configFromManifest(), or pass an NlConfig you assembled yourself.

<!-- android/app/src/main/AndroidManifest.xml -->
<application>

  <meta-data android:name="NITROPUSH_SERVER_URL"       android:value="https://app.nitropush.org" />
  <meta-data android:name="NITROPUSH_DEPLOYMENT_KEY"   android:value="PROD-KEY-…" />
  <meta-data android:name="NITROPUSH_STORAGE_BASE_URL" android:value="https://your-bucket.s3.amazonaws.com" />
</application>
// Option A: read the meta-data above via the SDK helper.
NitroPushSdk.shared.configure(NitroPushSdk.configFromManifest())

// Option B: assemble your own NlConfig (e.g. from BuildConfig fields).
//   app/build.gradle.kts:
//     buildConfigField("String", "NL_DEPLOYMENT_KEY", "\"PROD-KEY-…\"")
NitroPushSdk.shared.configure(
  NlConfig(
    serverUrl      = "https://app.nitropush.org",
    deploymentKey  = BuildConfig.NL_DEPLOYMENT_KEY,
    storageBaseUrl = "https://your-bucket.s3.amazonaws.com"
  )
)

EXPO_PUBLIC_* env vars get inlined in the bundle at build time. For bare React Native, use react-native-config.

With the NITROPUSH_* keys set in Info.plist / AndroidManifest (see the native tabs above), the no-arg configure() reads them for you. In your app entry:

import { configure, type NitroPushClient } from '@nitropush/react-native';

// Capture the returned client at module scope; every other call goes on it.
export const client: NitroPushClient = configure();

Self-hosted server or custom CDN? Use configureWith() with an explicit config instead — e.g. sourced from Expo public env vars:

# .env (Expo)
EXPO_PUBLIC_NITROPUSH_SERVER_URL=https://app.nitropush.org
EXPO_PUBLIC_NITROPUSH_DEPLOYMENT_KEY=PROD-KEY-Q1IEQH
EXPO_PUBLIC_NITROPUSH_STORAGE_BASE_URL=https://your-bucket.s3.amazonaws.com
import { configureWith, type NitroPushClient } from '@nitropush/react-native';

export const client: NitroPushClient = configureWith({
  serverUrl:       process.env.EXPO_PUBLIC_NITROPUSH_SERVER_URL!,
  deploymentKey:   process.env.EXPO_PUBLIC_NITROPUSH_DEPLOYMENT_KEY!,
  storageBaseUrl:  process.env.EXPO_PUBLIC_NITROPUSH_STORAGE_BASE_URL!,
});

For bare React Native, use react-native-config or babel-plugin-transform-inline-environment-variables.

Local dev with Expo Go. On Android emulators, localhost from the SDK points at the emulator itself, not your dev machine. Either use 10.0.2.2 (Android emulator’s host loopback) or your LAN IP. iOS sim shares the host’s localhost, so nothing special is needed.

4. Verify the install

Boot your app. The SDK won’t do anything user-visible until you call sync() (or its native equivalent), but you can opt-in to a verbose per-action trace from native code:

Toggle from AppDelegate.didFinishLaunchingWithOptions. Logs go to stdout (Xcode console + Console.app on a connected device).

NitroPushSdk.shared.setEnableLogs(true)
// → [NitroPush] configure serverUrl=… deploymentKey=… …
//   [NitroPush] checkForUpdate deploymentKeyOverride=(none)
//   [NitroPush] downloadUpdate releaseId=… label=… kind=codepush
//   …

Then tail it:

# Console.app → connected device → filter: NitroPush

Toggle from MainApplication.onCreate. Logs go to logcat tagged NitroPush.

NitroPushSdk.shared.setEnableLogs(true)
// → I/NitroPush: configure serverUrl=… deploymentKey=… …
//   I/NitroPush: checkForUpdate GET http://…/api/sdk/releases/latest?…
//   I/NitroPush: downloadUpdate releaseId=… label=… kind=codepush
//   …

Then tail it:

adb logcat | grep NitroPush

setEnableLogs(true) is off by default — leave it off in production. Every SDK action logs a line: configure, checkForUpdate, downloadManifestRelease GET <url>, installUpdate.<mode>, notifyAppReady, restartApp, rollback, lifecycle triggers (ON_NEXT_RESUME activation, etc.) and any failure with URL + status + body snippet.


Next: configure()