@dbrij/ship-react-native
v0.5.0
Published
Dbrij Ship code push for React Native: signed bundle updates with verify-before-apply and boot-canary rollback.
Readme
@dbrij/ship-react-native
Code push for React Native on Dbrij Ship: signed bundle updates, verify-before-apply, staged rollouts, and a boot canary that reverts a bundle which crashes on launch.
Install
npm i @dbrij/ship @dbrij/ship-react-native
cd ios && pod installExpo
Code push needs native code, so it runs in a development build or a store build, not in Expo Go. Flags, remote config and the kill switch (@dbrij/ship) work everywhere, Expo Go included.
npx expo install @dbrij/ship @dbrij/ship-react-native @react-native-async-storage/async-storageAdd the config plugin to app.json (or app.config.js):
{
"expo": {
"plugins": ["@dbrij/ship-react-native"]
}
}Then build as usual (npx expo prebuild, npx expo run:ios, eas build). On every prebuild the plugin points the release build at the newest verified Ship bundle, in AppDelegate (Swift or Objective-C) and MainApplication (Kotlin or Java). Debug builds keep loading from Metro. Nothing is edited by hand, so nothing is lost when prebuild regenerates the native folders.
One rule: use Ship code push or expo-updates, not both. Each decides which bundle the app boots, so the plugin stops the build if expo-updates is installed. To switch, run npx expo uninstall expo-updates.
If Expo changes its templates in a way the plugin does not recognise, prebuild fails with a message naming the file, rather than producing an app that silently ignores updates. Wire it by hand as below in that case, and tell us.
Then use it exactly as in Use.
Wire the bundle path (once, in the store build)
Not needed with the Expo plugin above. For a React Native app without Expo:
iOS — AppDelegate:
override func sourceURL(for bridge: RCTBridge) -> URL? {
#if DEBUG
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
return DbrijShip.bundleURL() ?? Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}Android — MainApplication (inside your ReactNativeHost):
override fun getJSBundleFile(): String? =
com.dbrij.ship.DbrijShipModule.getJSBundleFile(applicationContext)Also register DbrijShipPackage() in getPackages() (autolinking normally handles this).
Use
import { createShip } from '@dbrij/ship';
import { notifyAppReady, syncShipUpdates } from '@dbrij/ship-react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
const ship = createShip({
appKey: 'shp_...',
channel: 'production',
binaryVersion: '1.2.0',
platform: Platform.OS,
storage: AsyncStorage,
});
// In your root component, once the first screen mounts:
useEffect(() => {
notifyAppReady(ship) // disarms the boot canary + reports 'applied'
.then(() => syncShipUpdates(ship, { signPublicKeys: ['BASE64_KEY_FROM_DASHBOARD'] }))
.catch(() => undefined); // never let updates break the app
}, []);A staged (non-mandatory) update applies on the next launch. Mandatory updates restart immediately. A bundle that crashes before notifyAppReady is reverted automatically on the following start, and the failure is reported so the rollout can freeze itself server-side.
Safety model
- Bundles are verified on device: sha256 must match, and the ed25519 signature (made with your app's server-held private key) must verify against one of the
signPublicKeysyou pin here (every key the dashboard lists; two while the key is being changed). Unverified bytes are never written into place. - On Android below 13 the platform lacks Ed25519; the plugin refuses the update and the store binary keeps running (fail-closed).
- The store binary's bundled JS is always the fallback: revert never bricks.
