Deploy — Flutter OTA

Rollouts & safety

How Sankofa Deploy patches reach devices safely — staged rollout %, mandatory vs optional updates, rollback and roll-forward, and the boot-crash auto-rollback that restores a known-good app on its own.

A patch is a code change reaching real users, so Deploy gives you control over how fast it spreads and a safety net for when it goes wrong.

The update lifecycle

You don't have to write the update loop — sankofa init --deploy wires it into main(). Here's what actually happens on a device:

  1. Boot — apply what's staged

    SankofaUpdater.preFlight() runs before runApp. If a patch was downloaded on a previous run, it's applied now, then a "patch healthy" confirmation is scheduled ~10 seconds after the first frame.

  2. Check for a new patch

    The SDK checks the server for a newer patch on startup (and on resume). The check returns metadata only — label, size, signature, mandatory flag — no download yet.

  3. Download + verify

    An available patch downloads over HTTPS. The SDK verifies its SHA-256 and, when signing is enabled, its signature. A patch that fails verification is discarded.

  4. Stage — apply next cold launch

    A verified patch is staged on disk. The next cold launch applies it via preFlight(). Optional patches wait for a natural restart; mandatory patches are applied before the user continues.

To drive the check yourself (for a custom "Update available" prompt or a progress bar), use the SankofaUpdater API:

Dart
final updater = SankofaUpdater();

final result = await updater.checkForUpdate();
if (result.hasUpdate) {
  final update = result.update!;
  if (update.isMandatory) {
    await updater.downloadUpdate(update); // no prompt — download now
  } else {
    final ok = await showUpdateDialog(context, update);
    if (ok) {
      await updater.downloadUpdate(
        update,
        onProgress: (received, total) {
          setState(() => _progress = total > 0 ? received / total : 0);
        },
      );
    }
  }
}

checkForUpdate() resolves to one of a small set of statuses:

StatusMeaning
upToDateNo newer patch for this device.
outdatedA patch is available — see result.update.
unavailableCouldn't reach the server (DNS/TCP/timeout/5xx). Transient — try again next launch.
invalidConfigServer rejected the request — usually a mismatched engine_version or unknown app version.
bannedLocallyThe available patch matches one this device already auto-rolled-back. Refused until the local ban is cleared.

Staged rollout

Every patch has a rollout percentage — the share of eligible devices that receive it. Bucketing is deterministic per device, so a device that's in at 25% stays in as you ramp to 50% and 100%; you never re-shuffle who's exposed.

bash
# Ship to a slice, watch, then widen.
sankofa patch ios --rollout 10 --description "pricing hotfix"
sankofa patch ios --rollout 50
sankofa patch ios --rollout 100

Rollout defaults to 100% when you don't pass --rollout. Ramp when the change is risky; go straight to 100% for a confident one-line fix. Watch progress with sankofa status or the dashboard's Patches view. See the CLI reference for every flag.

Mandatory vs. optional

Optional (default)Mandatory
DownloadSilent, in the backgroundSilent, in the background
ApplyOn the next natural cold launchBefore the user continues
Use it forAlmost everythingCritical fixes only
Trade-offZero disruption; lands whenever the user relaunchesGuarantees fast adoption; interrupts the session
bash
sankofa patch ios --mandatory --description "fix data-loss on save"

Reach for mandatory sparingly — it's for a fix everyone must have now (a crash, a data-loss bug, a broken payment). Everything else should be optional and simply ride the next relaunch.

The boot-crash safety net

The most important guarantee in Deploy: a bad patch can't brick your app.

When preFlight() applies a patch, it starts a grace window. If the freshly-patched app crashes twice within 30 seconds of launch before it confirms healthy, the device:

  1. Disables the patch and restores the last known-good version (the prior patch, or the baseline release).
  2. Bans the bad patch label locally, so the SDK won't re-download the same patch and re-enter the loop.

This is entirely on-device — no server round-trip, no host code, and it works even if the device is offline. A patch that boots fine confirms healthy ~10 seconds after the first frame and the window closes.

Rollback and roll-forward

You have two ways to respond to a patch that's misbehaving in the field.

Rollback — revert the live patch

From the dashboard (or by pausing the rollout), stop serving the bad patch. Devices that have it restore the last known-good version on their next launch; devices that haven't taken it yet never will. Rollback returns your fleet to the previous good state.

Roll-forward — ship a corrected patch

Usually the better move: fix the code and publish a new patch. Because it's a new patch (new label/number), it isn't covered by any device's local ban list, so it reaches everyone — including devices that auto-rolled-back the broken one.

bash
# 1. Correct the code behind the same interface seam.
# 2. Ship it as a NEW patch (do not reuse the bad label).
sankofa patch ios --rollout 25 --description "pricing hotfix v2 (supersedes v1)"

Signing

Turn on patch signing so devices only apply patches you authored. Generate a project keypair once and register the public key:

bash
sankofa keys generate     # Ed25519 keypair for this project
sankofa keys register     # upload the public key so the server verifies at upload time

From then on, sankofa patch signs every patch, and both the server (at upload) and the SDK (at apply) verify the signature — an unsigned or wrong-key patch is refused. Back up the private key. Details in the CLI reference.

Next

Edit this page on GitHub