sbarbase
Contents

Recovery

What it is

The operator workflow provides daily environment backups, restore and optional encrypted S3-compatible offsite copy/fetch through backup and restore. It is implemented in lab/backup.py, lab/backup_offsite.py (encrypted run sets, the recommended off-site copy) and lab/offsite.py. Disposable local MinIO evidence is in docker-offsite-checks.json, and on 2026-10-17 a restore served only from a run set in Cloudflare R2 passed on the real server. The same workflow backs up the management database, recreates a backed-up environment on a new installation with sbarbase relink, and, as an attended step, rebuilds the whole database cluster from one backup run with lab/cluster_rebuild.py. Vault's root key belongs to the server and is not in a backup. Complete lost-server recovery from the off-site copy has not been exercised.

A separate historical cutover experiment stops one environment's writes, exports it encrypted, restores into an independent engine, checks the copy and switches traffic. The sequence and limits below describe that experiment, not every operator backup. Experimental native-dedicated recovery has additional engine/application/maintenance identity requirements and remains unaccepted; see the native placement contract and ledger.

Why the historical cutover uses a separate target

Choice: restore into an isolated target, verify, then switch. A restore that overwrites the live database in place leaves nothing to fall back to when it goes wrong. Restoring into a fresh engine keeps the source untouched (but fenced) until the copy has been checked, including identities, row-level security and old signed file URLs.

Rejected alternatives.

Historical cutover sequence

Five steps: stop writes, encrypted export, restore into an independent engine, verify, switch the route; if verification fails the source data is untouched and stays fenced until an operator reopens it, and exporting stops shared Storage for every environment on the engine

Verify before switching. Exporting still stops shared Storage for every environment on the engine.

  1. Fence. The environment's routing record is put in maintenance, so the gateway answers 503 instead of forwarding. Its three scoped service logins are switched to NOLOGIN and their original state is written to a journal, while the operator can still read and dump.
  2. Export. lab/cutover-export.py captures the database dump, the scoped logins and their memberships, database grants and settings, file bytes and extended attributes, the Storage tenant configuration and its URL-signing keys. The bundle is encrypted with AES-256-GCM; the key is stored separately, both mode 0600 and out of Git.
  3. Close. After the encrypted artifact is saved, the source database is set to refuse all connections. This is PostgreSQL state, so it survives restarts.
  4. Restore. A fresh, pinned PostgreSQL engine with a new administrator password, its own network and its own volume receives the dump in one transaction. Storage metadata and files are rebuilt, and signing keys are re-encrypted under the target's own key while keeping their key IDs.
  5. Verify. Table counts and content hashes are compared, scoped roles and grants are checked, original Auth and REST start against the copy, the original user logs in with the original password, reads rows protected by row-level security, and a signed URL issued before the export still downloads the same bytes.
  6. Switch. The new placement is staged on the routing record and routing is resumed with a revision check.
  7. Neighbours. Because shared Storage had to stop for the export, every environment on the source was offline during it. Unaffected environments now restart on the source while the moved environment stays pointed at the target.

Code: lab/source_fence.py, lab/cutover-export.py, lab/recovery_bundle.py, lab/recovery-restore-db.py, lab/recovery-check-services.py, lab/recovery_reconcile.py, lab/target_runtime.py, src/control/placement.ts. The operator procedure is in backup and restore.

Cutover fixture limits and remaining recovery gaps

Go deeper

Edit on GitHub