Releases: ship code, watch it land

This is the core loop. Once a release is uploaded, every device running your app picks it up on its next sync() call. Here’s the end-to-end flow.

1. Install the SDK

In your React Native or Expo project:

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

iOS: npx pod-install. Android: rebuild — the autolinking is already done.

For full SDK setup (including AppDelegate.swift and MainApplication.kt wiring), see the Nitro SDK install guide.

2. Configure the SDK in JS

Call configure() once at JS startup with the deployment key from your project’s environment row:

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

configure({
  serverUrl:       'https://app.nitropush.org',
  deploymentKey:   process.env.EXPO_PUBLIC_NITROPUSH_DEPLOYMENT_KEY!,
  storageBaseUrl:  'https://nitropush-bundles.s3.amazonaws.com',
});

storageBaseUrl is the public root of your storage bucket — see the Storage admin page for the exact value (it’s auto-shown once you’ve configured a provider).

3. Trigger a sync

sync() runs the full check-download-install flow:

import { sync, InstallMode } from '@nitropush/react-native';

await sync(
  { installMode: InstallMode.ON_NEXT_RESTART },
  (status) => console.log('status', status),
  (progress) => console.log(`${progress.receivedBytes} / ${progress.totalBytes}`),
);

Three install modes to choose from:

ModeWhen the new bundle activates
IMMEDIATERight now — sync() triggers a JS reload on completion.
ON_NEXT_RESTARTNext cold start. Recommended for most apps.
ON_NEXT_RESUMENext time the app comes to the foreground after minimumBackgroundDuration seconds.

For details on each, see sync() reference.

4. Confirm the install (rollback safety net)

In your top-level useEffect:

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

useEffect(() => {
  notifyAppReady();
}, []);

This is critical. If the app crashes before notifyAppReady() runs on a fresh install, the SDK assumes the new bundle is broken and rolls back to the previous version on the next launch. The lifecycle page covers the mechanics.

5. Generate a signing keypair and add the public key to the project

Bundle signing is an opt-in integrity check. When a project has a public key configured, the server rejects any release that doesn’t carry a valid signature from the matching private key — so a leaked deployment key alone can’t push tampered JavaScript to your users.

Generate an ECDSA P-256 keypair once:

# private key — keep this secret, never commit it
openssl ecparam -name prime256v1 -genkey -noout -out bundle-signing.pem

# public key (base64 DER) — paste this into your project settings
openssl ec -in bundle-signing.pem -pubout -outform DER | base64 | tr -d '\n'

Add bundle-signing.pem to your .gitignore immediately.

Then open your project’s Settings page and paste the public key output into the Bundle signing field:

Once the public key is saved, the server will reject uploads that aren’t signed with the matching private key.

6. Push your first release

From your laptop or CI, pass --signing-key pointing at your private key PEM:

# from a built dist directory containing your bundle + assets
npx nitropush release upload \
  --project       <project-id> \
  --environment   test \
  --platforms     android \
  --app-version   "*" \
  --label         v0.1.0 \
  --bundle-path   ./dist-android \
  --signing-key   ./bundle-signing.pem

The CLI prints Bundle signed ✓ before uploading. In CI, write the PEM from a secret to a temp file and export NITROPUSH_BUNDLE_SIGNING_KEY=./bundle-signing.pem instead of using the flag — it’s picked up automatically.

The CLI:

  1. Hashes the bundle and every asset (SHA-256).
  2. Dedupes against existing assets in your bucket — only new bytes upload.
  3. Writes a manifest pointing at the content-addressable storage keys.
  4. Persists a release row in the org / project / environment.

The full reference for release upload (and other release subcommands) is in CLI overview.

7. Watch it land

Open the project page in the dashboard:

You’ll see:

  • The new release row with its label, environment, platforms, and rollout %.
  • Live download / install / failed-install counts as devices pick it up (events stream in from Swift + Kotlin natively).
  • A delivery summary for every connected notification integration.

Triggering a re-sync from a test device is the fastest end-to-end test:

await sync({ installMode: InstallMode.IMMEDIATE });

If your sync log shows update_check → download_started → download_completed → install_completed, you’re shipping.

8. Adjust rollout (gradual rollout)

By default a release goes out at 100% — every device sees it on its next sync. To stage a rollout, edit the release’s rollout percentage from the project page. The SDK’s bucketing is deterministic per clientUniqueId — a device that’s in the 25% bucket stays in the 25% bucket as you ramp, so a device either gets a release every time or never (until the percentage grows past their bucket).

9. Promote across environments

Tested in test, ready for stage? Click Promote on a release row:

  • A new release row gets created in the target environment.
  • Same label + same bundle hash — no re-upload, no re-deploy.
  • A release.promoted notification fires for any matching integrations.

The release detail page also exposes a Rollback button that flips the environment’s pointer back to the previous release. Devices on the rolled-back release will pick up the previous version on their next sync.


That’s the loop. From here: