Over-the-air updates through One.Updates, One’s own OTA client. The native
launcher selects the bundle before React Native starts: the newest downloaded
update for the binary’s runtime version, or the embedded bundle. A bad update
never bricks the app: a fatal error before first render marks the update
failed and the host reboots onto the next candidate in the same session.
import { One } from 'one'
// values fixed for the process, read once at importOne.Updates.isEnabled // booleanOne.Updates.runtimeVersion // string | nullOne.Updates.updateId // string | nullOne.Updates.isEmbeddedLaunch // booleanOne.Updates.createdAt // Date | nullOne.Updates.manifest // UpdateManifest | null
const check = await One.Updates.check()// { type: 'available', manifest } | { type: 'none' }
const fetched = await One.Updates.fetch()// { type: 'fetched', manifest } | { type: 'none' }
One.Updates.getStaged() // UpdateManifest | null
const subscription = One.Updates.addStagedListener((staged) => { console.log(staged?.id ?? 'none')})subscription.remove()
await One.Updates.reload()updateId is the embedded id on an embedded launch; manifest is null
there. createdAt is a Date; the manifest’s own createdAt stays the
ISO string it arrived as, and publisher metadata rides in
manifest.metadata as JSON scalars. check reports available only when
the served update is newer than the running one and not already staged.
fetch downloads the bundle and every asset, verifies every sha256, and
stages the update atomically; reload restarts the host onto the newest
ready update.
one({ native: { app: { // ... updates: { url: 'https://updates.example.com', runtimeVersion: '1.0', }, }, },})Prebuild points the release bundle at the launcher and stamps the url and
runtime version into the binary. A build without updates.url launches its
embedded bundle with isEnabled false, and check, fetch, and reload
reject E_UPDATES_DISABLED. In debug builds the launcher is off and the
bundle keeps coming from the dev server. Network failures reject
E_UPDATES_CHECK; a file that arrives but fails its hash check rejects
E_UPDATES_FETCH and stages nothing.
Expo-prebuilt apps pass the same updates object to vxrn/expo-plugin and
leave expo-updates out. It covers iOS only: Expo’s Android host builds its
own ReactHost delegate, which the launcher cannot re-point, so Android apps
use one prebuild.
{ "expo": { "plugins": [ ["vxrn/expo-plugin", { "updates": { "url": "https://updates.example.com", "runtimeVersion": "1.0" } }] ] }}Publish from the app’s source with the same bundler and options as the embedded build:
one updates publish --platform ios --out ./ota-outone updates publish --platform android --out ./ota-out-androidEach run writes a fresh directory: manifest.json plus content-named files
under assets/. Serve it at <url>/<platform>/<runtimeVersion>/, so the
manifest lands at <url>/ios/1.0/manifest.json for the config above. Asset
urls may be relative to the manifest. --metadata key=value (repeatable)
adds publisher metadata; values parse as JSON scalars when they look like
one.
For local symbolication, pass --intermediates-out ./ota-debug alongside
--out. One writes bundle.js and a composed Hermes source map there.
Keep this directory separate from the published output and never upload it:
the map can contain application source text. Publish only manifest.json
and assets/ from --out. Requesting a Hermes map changes the generated
bytecode, so use the map only with the bundle from that same publish run.
Separate publishes can have different bytecode hashes from the generated
loader cache key and the temporary bundle path that Hermes embeds. Pin
ONE_CACHE_KEY and compile the same plain bundle from a fixed filename when
checking whether compiler options change bytecode. The normal publish path
uses a fresh temporary directory so concurrent runs stay isolated.
After a launch succeeds the launcher keeps the running update, the newest older update as the rollback spare, and a staged update newer than the running one, and deletes every other download. Downloads for another runtime version, or older than the embedded update, are deleted at launch and never fetched.
On web the reads are the empty values, check and fetch reject because
there is no native side, getStaged returns null, the listener never
fires, and reload reloads the page.
Edit this page on GitHub.