Update lifecycle & events
Exactly what happens from check to activation — install modes (restart / resume / immediate), the full event funnel, the boot-guard healthy window, mandatory updates, delta patches, and how to drive a custom update UI from events.
This page is the detail behind "the SDK downloads and applies an update." If you're building an update UI, wiring telemetry, or reasoning about when code actually swaps, this is the contract.
The event funnel#
Every update walks a fixed sequence of events, emitted on otaEventEmitter. Each is
a stage in a funnel — the earlier ones always fire, the later ones only if the
update progresses:
| Event | Meaning |
|---|---|
CHECK | A check was made against the server — the denominator of the funnel |
OFFERED | The server has a bundle for this device — the offer, before any bytes move |
DOWNLOAD_STARTED | Download began |
DOWNLOAD_PROGRESS | Progress tick (0–1); high-frequency |
DOWNLOAD_COMPLETE | Bytes are down and verified |
INSTALLED | Staged and swapped — emitted optimistically, before the bundle has booted |
BOOT_SUCCESS | The bundle booted and survived to the healthy mark — the honest activation signal |
APPLY_FAILED | Staging/applying failed |
AUTO_ROLLBACK | The boot guard reverted a bundle that failed to become healthy |
MANUAL_ROLLBACK | An operator rolled the release back |
import { otaEventEmitter } from '@scalebun/react-native';
const off = otaEventEmitter.on('BOOT_SUCCESS', (e) => analytics.track('ota_activated', e));otaEventEmitter.on('AUTO_ROLLBACK', (e) => analytics.track('ota_rolled_back', e));// later: off();Install modes — when an update activates#
A staged bundle never swaps mid-session. When it activates is set per release via
its install mode (absent means ON_NEXT_RESTART):
| Install mode | Behaviour |
|---|---|
ON_NEXT_RESTART (default) | Nothing happens now; the next cold start loads the new bundle |
ON_NEXT_RESUME | The JS runtime restarts the next time the app returns to the foreground |
IMMEDIATE | The JS runtime restarts right after install |
You can also force an immediate restart from the client regardless of the release's
mode, with autoRestart on the hook (or restart()):
useOtaUpdate({ /* … */ autoRestart: true }); // apply as soon as it's stagedThe boot guard & the healthy window#
When a new bundle activates, the SDK arms a boot guard: it waits for the bundle to prove it's healthy before committing to it. If the bundle crashes on startup before that, the SDK reverts to the last-good bundle on the next launch — a broken release can't brick the app.
The healthy window is configurable when the orchestrator initializes:
import { otaOrchestrator } from '@scalebun/react-native';
// healthyAfterMs: how long the new bundle must run before it's marked healthy.otaOrchestrator.init({ healthyAfterMs: 10_000 }); // default 10sSurvive
healthyAfterMsafter boot → the marker clears,BOOT_SUCCESSfires, the bundle is committed.Crash before that →
AUTO_ROLLBACK, and the device reports it so the rollback is visible in the dashboard's recovery view.
The boot-attempt limit (how many crashed launches before reverting) lives in native code — the guard is enforced below JS so a JS crash can't disable it.
Mandatory updates#
A release can be marked mandatory. The hook surfaces it, and with mandatoryBlocksUi
the SDK blocks the UI until it's applied:
const { mandatoryUpdatePending, downloadProgress } = useOtaUpdate({ /* … */ mandatoryBlocksUi: true });if (mandatoryUpdatePending) return <BlockingUpdate progress={downloadProgress} />;Use this sparingly — a mandatory update that blocks the UI is a hard gate on your users. It's the right tool for a critical fix, not routine releases.
Delta (patch) updates#
When the server can produce a binary diff between the device's current bundle and the
new one, the SDK downloads the patch instead of the whole bundle and reconstructs
it locally — far less data on the wire. It's automatic; you don't opt in. The
DOWNLOAD_COMPLETE event tells you whether a patch was used:
otaEventEmitter.on('DOWNLOAD_COMPLETE', (e) => { console.log(e.patchUsed ? 'delta patch applied' : 'full bundle downloaded');});Runtime compatibility (why targeting matters)#
A bundle carries the RN version and Hermes bytecode (HBC) version it was built against. The device checks these before loading — an OTA bundle built for a Hermes engine the installed binary doesn't have would fail to load. This is why:
Publish with the same JS engine as your store build (
--hermes, the default).Scope releases to compatible native versions with
--target-version— a JS bundle that needs a native module from app1.2.0must not reach a device on1.1.0.
See Channels & rollouts → targeting.
Error handling#
sync() resolves to a SyncResult whose status is one
of UP_TO_DATE | UPDATE_INSTALLED | ROLLED_BACK | ERROR. Treat ERROR (and the
APPLY_FAILED event) as non-fatal — the app keeps running the current bundle; log it
and let the next check retry:
const r = await sync();if (r.status === 'ERROR') logger.warn('OTA sync failed', r.error); // current bundle keeps runningNext#
Quick start — the
useOtaUpdatehook these events back.Channels & rollouts — the crash-rollback and targeting in context.