Skip to Content
ConceptsLaunch Safety & Rollback

Launch Safety & Rollback

A bad OTA update must never leave users stuck, and a good one must never be rolled back because of something that was not observed. Norrix follows the same model as expo-updates: every launch decision is a pure function of a persisted state document, and that document changes only when a concrete event was witnessed.


The state document

Each app keeps one file, written atomically, at Documents/norrix_ota/state.json on iOS and <filesDir>/norrix_ota/state.json on Android. It records:

FieldMeaning
binaryThe store binary the state belongs to (version, build, fingerprint). A different binary discards everything.
updatesInstalled updates with successfulLaunches and failedLaunches counters.
quarantinedUpdate ids that failed before ever succeeding on this binary. They are never launched or installed again.
lastLaunchWhat the loader booted for the current process and its outcome: started, displayed or failed.

The native loader (NorrixOTA) and the JS SDK both read and write this document with the same rules.


Which bundle boots

At every launch, and before every in-process reload, the loader picks the newest installed update that:

  1. is not quarantined,
  2. has launched successfully at least once or has never failed, and
  3. has a complete install on disk (an app/ directory plus the norrix-install.json marker written at the end of the install).

If nothing qualifies, the store bundle boots. The loader records lastLaunch with outcome started and nothing else.


Launch outcomes

Only witnessed events change an outcome:

EventOutcome
First content displayed (displayed, or a window’s contentLoaded on core 9.1+)displayed; successfulLaunches increments.
Uncaught JS error before first displayfailed; the update is quarantined and the app relaunches into the last known-good bundle.
iOS bootstrap never reaches UIApplicationMain, or an uncaught native exceptionfailed, recorded natively; the next launch picks the last known-good bundle.
Android uncaught exception during startupfailed, recorded natively; the next launch picks the last known-good bundle.
Process ends any other way (user kill, low memory, watchdog, iOS prewarm, background)Nothing. The next launch boots the same bundle again.

An update that has succeeded before is not blamed for a later crash, so an established update is never quarantined by an unrelated failure.

Success is observed by the SDK at module evaluation, which is why @norrix/client-sdk should be imported at the top of your entry file. Initializing later only delays the confirmation; it can no longer cause a rollback.


Recovery

When a failed launch is observed in JS, the SDK:

  1. Marks the update failed and quarantines it.
  2. Waits up to five seconds for a newer update if one can be fetched.
  3. Relaunches into the newest launchable bundle: the previous update if it is still on disk, otherwise the store bundle. The relaunch happens in-process when the runtime exposes NativeScriptRuntime.reloadApplication (the @nativescript/ios 9.1.0 release does not; a 9.1.x patch adds it); otherwise the process restarts, and the cold launch reaches the same decision.
  4. Reports ota_launch_failed and ota_recovered telemetry and emits SyncStatus.ROLLED_BACK to your status callback.

Later update checks send the quarantined ids as excludeIds, so the server never offers the failed update to that device again. A new store binary clears the quarantine. Set retryFailedUpdates: true to opt out while debugging a rollout.


What you see in the dashboard

  • Each update card shows how many distinct devices reported a failed launch of it.
  • The OTA analytics page counts failed launches, recoveries and confirmed launches, and lists failures per update id.

Use getRunningUpdate() in the app to show the bundle that is actually executing, and getQuarantinedUpdateIds() to inspect a device’s quarantine. See the SDK usage guide for code.