Guide · 11

Ship new code.

Deploying new code recomputes derived values. Large apps can prepare the new version in the background, then switch over. Stored records change only when your own code migrates them.

Deploy computation changes

A change to a derive or aggregate does nothing until you build and deploy the bundle. You have two options:

  • Direct deploy recomputes stored derived values in one step, then switches code and results together. Fine for small apps.
  • Staged deploy rebuilds in pages while the current version keeps serving, then switches when you activate it. Use it for large rebuilds.
  1. Stage. stageDeployment(bundle, {requestId}) records the new bundle and any new indexes.
  2. Advance. Each advance call backfills new indexes, then rebuilds a page of derived values in the background. Requests keep using the current code.
  3. Ready. Once the rebuild is done, ordinary writes keep the new graph up to date until you activate or cancel.
  4. Activate. activate switches bundle, aliases, policy, keys, indexes and derived state in one step.
  5. Collect. Call collect until the phase is collected to free the old graph. Finish this before the next deployment.
  • Use a unique request ID for each deployment and save it with the bundle. Progress survives leader changes, restarts and partition moves.
  • Before resuming, check that both the request ID and bundle hash match, rather than staging again.
  • Nothing runs on its own: your code calls advance and activate. The progress counters show work done, not a percentage.
Start or resume one deployment
const requestId = "application-upgrade-v2"; // Save and reuse for this bundle.
const maxBytes = 256 * 1024;
let build = (await client.stagedDeploymentStatus()).value;
if (build === null || build.phase === "collected") {
  build = (await client.stageDeployment(bundle, {requestId})).value;
}
if (build.requestId !== requestId || build.bundleHash !== bundle.hash) {
  throw new Error("Existing deployment ID/bundle does not match this intent");
}
while (build.phase === "backfill" || build.phase === "rebuilding") {
  build = (await client.controlStagedDeployment({
    operation: "advance", requestId, maxBytes,
  })).value;
}
if (build.phase === "failed") {
  throw new Error(build.error ?? "Cancel and collect the failed target");
}
if (build.phase === "ready") {
  build = (await client.controlStagedDeployment({
    operation: "activate", requestId,
  })).value;
}
while (build.phase === "active" || build.phase === "canceled") {
  build = (await client.controlStagedDeployment({
    operation: "collect", requestId, maxBytes,
  })).value;
}
  • Uncertain response: read the status and continue with the same request ID. Aborting a request does not undo committed progress.
  • Page or activation error: earlier progress is kept and the active app doesn’t change. Adjust the budget and retry, or cancel and collect.
  • Phase failed: a write hit a fatal error while maintaining the new graph. The job can’t activate; cancel and collect it.
  • Canceled: a canceled deployment can’t be activated. After collecting, stage corrected code with a new request ID.
  • No rollback: after activation, fix forward with another deployment.
  • While a cross-group transaction is prepared, deployment controls return TRANSACTION_PREPARED. Status still works.

Tune rebuild pages

Each page blocks writes while it prepares and commits; reads continue. Smaller pages mean shorter write stalls and a slower rebuild.

  • FLOWER_DEPLOYMENT_PAGE_MS sets the target time per page. Default 200 ms, capped by FLOWER_EVALUATION_TIMEOUT_MS. Try 20 ms if write latency matters, and measure.
  • maxBytes on each request limits page size. It defaults to the transaction byte budget.
Each server · independent startup settings
export FLOWER_DEPLOYMENT_PAGE_MS=20
export FLOWER_WRITER_BATCH_MS=50
# Start or restart each Flower node with these settings.
  • These are startup settings. Restart nodes to apply them. Ordinary write batching is unaffected.
  • The page target is not a hard deadline. A page can take the target plus a full evaluation timeout.
  • One derived value and the new dependencies it pulls in must fit in one evaluation. The whole graph’s metadata must fit in memory.
  • Until you activate, every write also maintains the new graph, and the extra graph uses storage until collected.

The staged deployment benchmark shows the throughput and latency tradeoff. Those runs used an older shared setting, so treat them as rough.

Migrate stored records

Deploying rebuilds derived values. It does not change stored records. During a staged deployment, old and new code share the same records, so both must read every record shape.

  1. Deploy compatible code. Read old and new shapes, write only the new shape, and add a mutation that converts one bounded batch.
  2. Save progress with the data. In that mutation, read a cursor from a migration record, convert a page, and save the next cursor with the changed rows. Check each row’s version so a retried batch doesn’t convert twice. Reuse the request ID when retrying an uncertain batch.
  3. Page by key. Use ctx.scan(rows, {gt: afterKey, limit: 100}), leaving out gt on the first page. An empty page means you’re done. For an indexed subset, use ctx.range on fields the migration doesn’t change. Avoid offsets and unbounded scans.
  4. Verify, then clean up. Check that no old rows remain. Then deploy code without the compatibility path and remove old fields with bounded mutations.
  • Names are identities. Renaming a collection or derived value, or changing a TypeScript type, doesn’t touch existing data. Copy data explicitly.
  • Protect migration mutations with application authorization. An operator token doesn’t make a public mutation admin-only.
  • During a staged deployment, each migration batch also updates the new graph. Size batches for that.

See the range and cursor rules, mutation context and staged deployment API. Moving a partition between Raft groups keeps data and code as they are; it is not a migration.