OTA updates
Publish JavaScript-only updates to installed Expo and bare React Native apps with Ubriot, manage channels and roll back.
Over-the-air (OTA) updates ship JavaScript and asset changes to installed apps without waiting for store review. Anything that changes native code still needs a new build with a new runtime version.
How updates reach a device
- An update is one export of your JavaScript and assets, published for iOS, Android or both. Each publish creates an update group.
- A channel, such as
production, points at one update group. A binary asks for updates on the channel it was built with. - A runtime version ties an update to the native code it was built against. A device only receives updates published for its own runtime version.
Expo apps
Expo apps use stock expo-updates. Point it at your Ubriot manifest URL and the channel the binary should follow:
{
"expo": {
"runtimeVersion": "1.0.0",
"updates": {
"enabled": true,
"url": "https://api.ubriot.dev/api/v1/ubriot/updates/manifest?appId=your-app-slug",
"requestHeaders": { "expo-channel-name": "production" }
}
}
}Replace your-app-slug with the slug of your app in Ubriot. Moving from EAS Update or App Center CodePush? ubriot migrate eas or ubriot migrate codepush makes these changes for you; both are a dry run until you pass --apply.
New binaries must include this URL. Existing installs keep their previous update host until users install a build that has it.
Publish an update
From your app folder:
ubriot update --channel production --message "Fix checkout bug"ubriot update runs expo export locally, uploads the result and publishes it. It reads the app slug from expo.slug and the runtime version from runtimeVersion in app.json, or takes --app and --runtime-version. If your app uses a runtime version policy such as fingerprint or appVersion, pass the resolved value with --runtime-version. The message defaults to the current git short SHA.
Before publishing, ubriot update shows how many devices the runtime version reaches and asks you to confirm when the reach looks wrong (requires ubriot 1.3.0 or later). It also warns when your local Node.js is too old for the project's Expo SDK (requires ubriot 1.4.0 or later). Pass --yes to skip both questions. In a non-interactive shell, such as CI, the CLI prints the warnings and continues without asking.
On its way to npm
Other useful options:
--platform iosor--platform androidpublishes for one platform. The default isall.--no-exportpublishes an existing export directory, set with--export-dir(defaultdist).ubriot updates publishpublishes an export you built yourself; it needs--app,--channeland--runtime-version.
Channels and rollback
ubriot channels list --app myapp
ubriot channels create --app myapp --name staging
ubriot updates list --app myapp
ubriot channels point --app myapp --channel production --update-group 42Pointing a channel at an earlier update group rolls devices back to it. You can do the same from Updates in the dashboard.
Bare React Native apps
Bare React Native apps (no Expo) use the ubriot-updates-rn client. It downloads the update bundle and every asset in the background, checks each file's hash, swaps the update in on the next launch, and rolls back to the last good bundle if an update fails to boot. A failed download or check never blocks app start.
Version
The examples here describe ubriot-updates-rn 0.4.0. Code signing needs 0.3.0 or later, and React Native 0.88 needs 0.4.0 or later.
On its way to npm
Install
npm install ubriot-updates-rn
cd ios && pod installThe native module autolinks on iOS and Android. Rebuild the native app after installing. It needs React Native 0.76 or newer.
JS API
Import the package once at the top of your entry file (index.js or App.tsx), so its asset resolver is installed before any font or image is resolved.
import * as UbriotUpdates from "ubriot-updates-rn";
UbriotUpdates.configure({
apiBase: "https://api.ubriot.dev/api/v1",
appSlug: "your-app-slug",
channel: "production",
runtimeVersion: "your-app-1.0.0",
});
const result = await UbriotUpdates.sync();sync() checks, downloads and stages an update, and returns "up-to-date" or "updated". The update applies on the next launch. To drive each step yourself:
const { available } = await UbriotUpdates.checkForUpdate();
if (available) {
const { ready } = await UbriotUpdates.downloadUpdate();
if (ready) await UbriotUpdates.applyUpdate({ restart: true });
}configure() also takes optional nativeBuildId and nativeBuildCreatedAt (a UTC ISO string), which let Ubriot tell a stale native build apart from a stale update, and requireCodeSigning (see OTA code signing).
A recommended app start:
function Root() {
useEffect(() => {
UbriotUpdates.configure(updatesConfig);
UbriotUpdates.sync().catch(() => {});
}, []);
return <App onFirstScreenRendered={() => UbriotUpdates.markLaunchSuccessful()} />;
}Rollback model
- A freshly applied bundle is active but unverified.
- On the first launch of an unverified bundle, a launch counter is saved before JavaScript runs.
- If the app boots that bundle again while it is still unverified, meaning the previous launch never reached
markLaunchSuccessful(), the client goes back to the last known good bundle, or the embedded one. markLaunchSuccessful()marks the running bundle verified and makes it the new last known good.- A rolled back update is remembered and never downloaded again, so a bad update cannot loop.
Call markLaunchSuccessful() from the first screen that proves the app works, not from a timer on the splash screen: a timer marks a hung update as good. markLaunchSuccessfulAfter(ms) exists only as a fallback for apps that cannot hook a render, and returns a function that cancels it.
Native integration
Point the app at the bundle the client resolves, so downloaded updates load in release builds.
iOS, in your AppDelegate (Swift needs a bridging header that imports <UbriotUpdatesRn/UbriotUpdates.h>):
override func bundleURL() -> URL? {
#if DEBUG
RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
UbriotUpdates.bundleURL()
#endif
}Android, in MainApplication:
import dev.ubriot.updates.UbriotUpdatesModule
override val reactHost: ReactHost by lazy {
getDefaultReactHost(
context = applicationContext,
packageList = PackageList(this).packages,
jsBundleFilePath =
if (BuildConfig.DEBUG) null
else UbriotUpdatesModule.resolveBundleFilePath(applicationContext),
)
}Both return the verified downloaded bundle when there is one, applying the rollback rules, and otherwise fall back to the bundle embedded in the build. On the classic architecture, return resolveBundleFilePath(context) from ReactNativeHost.getJSBundleFile().
React Native 0.88 and Expo SDK 58
ubriot-updates-rn 0.4.0 builds and type-checks against React Native 0.88. The SDK 58 scene life cycle needs no changes, since the release bundle is still chosen in bundleURL(). The Android library ships R8 keep rules that apps pick up automatically. Moving to SDK 58 is a native change, so give the new build a new runtime version. See Expo SDK 58.
On its way to npm
Publish for a bare app
ubriot update --app your-app-slug --channel production --runtime-version your-app-1.0.0 --message "What changed"The CLI detects a bare project and bundles it with the React Native bundler instead of expo export; force it with --bare, and set the entry file with --entry-file. Bare bundling requires ubriot 1.3.0 or later. The runtime version you publish must match the runtimeVersion passed to configure().
On its way to npm