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:
| Mode | When the new bundle activates |
|---|---|
IMMEDIATE | Right now — sync() triggers a JS reload on completion. |
ON_NEXT_RESTART | Next cold start. Recommended for most apps. |
ON_NEXT_RESUME | Next 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:
- Hashes the bundle and every asset (SHA-256).
- Dedupes against existing assets in your bucket — only new bytes upload.
- Writes a manifest pointing at the content-addressable storage keys.
- 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.promotednotification 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:
- CLI overview — every flag and the auto-detection logic between CodePush-style and Expo-style bundles.
- Nitro SDK → sync — install modes, progress listeners, error handling.
- Nitro SDK → lifecycle —
notifyAppReady, rollback, manual restart.