Backups workflow

This guide walks the everyday backup commands. The mental model behind them — what a backup contains per engine, the three trust states, what backups guarantee — lives in Backups (concept). Read that page first if any of those terms are new.

What you will learn

  • How to create, list, verify, and download backups from the CLI
  • How to restore a backup (engine-specific mechanics)
  • The pre-destructive workflow pattern most projects need
  • How to wire backups into CI before staged deploys
  • How to read trust labels and recover when they say something is wrong

Prerequisites

  • The Flux CLI installed and logged in (flux login).
  • A project's slug and seven-character hash from flux list (or a flux.json carrying both — see Configuration).
  • Disk space for the artifact when you download — pg_dump -Fc archives are compressed but can still be substantial for active projects.

1) Create a backup

Both engines use the same command. The control plane runs pg_dump server-side and stores the artifact in its own backup volume:

bash
flux backup create --project bloom-atelier --hash 0a1b2c3

On v1 dedicated the artifact is the full project database. On v2 shared it is the tenant API schema only (pg_dump -Fc --schema=t_5ecfa3ab72d1_api --no-owner --no-acl). Both run in the same command shape; the engine determines the contents.

Creating a backup does not make it trustworthy. The fresh artifact is in the artifact validated state at best — the file exists and looks structurally fine. Promoting it to restore-verified is a separate step (see 3).

A common shortcut is to chain create and verify:

bash
flux backup create --project bloom-atelier --hash 0a1b2c3 \  && flux backup verify --project bloom-atelier --hash 0a1b2c3 --latest

This is the single most useful one-liner before any destructive action.

When the operator has enabled R2 offsite replication, flux backup create also reports offsite upload status. Offsite storage is not a substitute for restore verification — always run flux backup verify before destructive work. Retention that removes a restore-verified local row also deletes that backup's R2 object (or filesystem offsite copy). Historical orphans uploaded before this alignment may still need a one-time bucket cleanup.

Self-hosted operators can optionally email backup-scheduler failures (freshness pipeline, offsite, artifact validation, tick hard-fail) via Resend (primary) or SMTP (fallback). The same env can page on host/Docker errors when FLUX_OPS_WATCH_ENABLED=1. See Email alerts for scheduler / ops failures. Unset send provider is a no-op and does not affect backups. Cloudflare Email Routing cannot send outbound mail.

2) List backups

bash
flux backup list --project bloom-atelier --hash 0a1b2c3

The output shows recent backups newest-first with their trust labels (per the three trust states). A status block at the top summarizes the latest backup, verification state, and whether destructive actions are allowed:

text
Latest backup:            1781975913697 · 2026-06-20T17:18:33.697Z · Jun 20, 2026, 5:18 PM UTC · 2 days agoVerification:             Restore-verifiedSafe destructive actions: Allowed

Timestamps in CLI output always show Unix ms · ISO UTC · human UTC · relative so machine and operator forms stay aligned (see docs/ARCHITECTURE-CONTRACT.md § CLI timestamps).

Common trust labels in the history list:

  • Restorable — restore-verified, safe to act on.
  • Created, not restore-verified — file exists and looks valid; nobody has tried to restore it yet.
  • Restore verification failed — the artifact exists but pg_restore did not reproduce a usable database. Treat as broken.
  • Validating backup artifact — the upload finished moments ago and the validator is still catching up. Wait briefly and re-list.
  • No backups — exactly what it says; create one.

Pass --verbose for catalog timestamps, artifact paths, and the underlying tier names. Useful when a label looks wrong and you need to trace it.

List output also includes platform minimum backup freshness when the control plane exposes it: whether the newest restore-verified backup is within the platform interval (default seven days). Overdue freshness is informational — destructive actions still use the restore-verified gate on the latest backup, not freshness alone.

3) Verify a backup

This is the only step that promotes a backup to restorable. Verification runs pg_restore against the artifact in a disposable Postgres container on the control plane and checks that the tenant (or dedicated) schema came back. Before that restore, Flux creates NOLOGIN stubs with no superuser and no BYPASSRLS for every non-reserved role the archive names — including t_<shortId>_role and t_<shortId>_ddl — so CREATE POLICY ... TO t_<shortId>_ddl and ALTER ... OWNER TO can be applied. The container is removed when verification finishes, which drops the stubs. pg_restore must exit 0 with no ignored errors; a missing role is not skipped. A non-empty dump must restore at least one non-system table. A schema-only empty v2 tenant export (zero user tables) is still restore-verified when the expected t_<shortId>_api schema is present after restore and the dump TOC lists that schema with no TABLE entries — emptiness has to match the dump, not look like a failed restore:

bash
flux backup verify --project bloom-atelier --hash 0a1b2c3 --latest

Verify the latest with --latest, or a specific row with --id <backupId>. If verification fails, the row's trust label flips to Restore verification failed and the underlying error is recorded in the catalog — flux backup list --verbose will show it.

Restore verification is the contract. A backup that has never been verified is, operationally, not a backup yet. The pre-destructive pattern in Section 6 is built around this idea.

4) Download a backup

bash
flux backup download --project bloom-atelier --hash 0a1b2c3 --latest -o ./bloom.dump

Pass --latest (newest backup) or --id <backupId> (a specific row). The artifact is binary (pg_dump -Fc); the CLI refuses to write it to a terminal without -o or a redirect to avoid garbling the shell:

bash
# Equivalent — redirect instead of -oflux backup download --project bloom-atelier --hash 0a1b2c3 --latest > ./bloom.dump

The downloaded file is a standard Postgres custom-format archive. pg_restore --list ./bloom.dump works against it without any Flux-specific tooling.

5) Restore a backup

There is no flux backup restore command today. Restoring is a two-step manual flow on both engines, deliberately so — the engines have different restore targets:

v1 dedicated — restore into the project's own Postgres

Get the project's Postgres connection string from the CLI:

bash
flux project credentials bloom-atelier --hash 0a1b2c3# Copy the Postgres URI under the "Postgres" section.

Run pg_restore from your machine:

bash
pg_restore --clean --if-exists --no-owner --no-acl \  --dbname "<paste Postgres URI>" \  ./bloom.dump

--clean --if-exists drops existing objects before recreating them. Skip those flags if you want to overlay onto an empty database instead.

v2 shared — restore into a Postgres you control

A v2 backup is a tenant export, not a shared-cluster restore. The intended targets are:

  • A v1 dedicated stack you are migrating to (flux migrate does this for you — see Pooled → dedicated migrate).
  • Your own Postgres for archival, off-platform analysis, or running locally.

The restore is a normal pg_restore against any Postgres with the same major version; the dump is portable:

bash
pg_restore --no-owner --no-acl \  --dbname "postgresql://user:pass@host/your_database" \  ./bloom.dump

pg_dump --no-acl omits GRANT and ALTER DEFAULT PRIVILEGES from the archive. pg_dump --no-owner does not remove ownership from a custom-format archive; pg_restore --no-owner is what suppresses ALTER ... OWNER TO. CREATE POLICY ... TO <role> is neither an owner command nor an ACL, so it is always in the file. Flux backup verification passes --no-owner --no-acl and still has to stub those policy roles. A restore that does not pass --no-owner (including flux db restore on v1, and a manual pg_restore without that flag) also applies OWNER TO t_<shortId>_ddl and needs the same stub.

Before pg_restore into a database that does not already have the tenant roles, create them as NOLOGIN with no superuser and no BYPASSRLS. At minimum: t_<shortId>_role and t_<shortId>_ddl (same short id as t_<shortId>_api), plus anon, authenticated, service_role, and authenticator if the archive names them. Leave the stubs in place on a database you will serve: the restored policies reference them. Re-create grants before serving traffic when the archive was taken with --no-acl, because those grants were not written into the file. flux db restore (v1 dedicated) and v2→v1 migrate create these stubs themselves. flux db restore does not pass --no-acl or --no-owner, so policies and any ownership commands in the archive are kept.

6) Pre-destructive workflow pattern

The most common reason to use Flux backups in day-to-day work is "back up before doing something risky." The pattern is the same regardless of engine:

bash
# 1. Create + verify in one shotflux backup create --project bloom-atelier --hash 0a1b2c3 \  && flux backup verify --project bloom-atelier --hash 0a1b2c3 --latest# 2. Confirm the latest is restore-verifiedflux backup list --project bloom-atelier --hash 0a1b2c3 | head -3# 3. Do the risky thing — apply a migration, drop a column, run a destructive scriptflux push ./db/migrations/0042_drop_legacy_table.sql# 4. If something is wrong, the verified backup is the recovery path

This pattern is the same one V1 dedicated quick SQL and Pooled → dedicated migrate link to. It is not engine-specific; it is destructive-action-specific.

flux nuke enforces the pattern by default — it refuses to remove a project unless the latest backup is restore-verified. The override (--skip-backup-check) exists for cases where you genuinely want to destroy without a recovery path; the warning text on stderr makes the choice explicit.

7) CI pattern: backup before staged deploy

Wiring this into CI is straightforward. The shape that works for most teams:

yaml
# Pseudocode — adapt to your CI runner- name: Backup production project  run: |    flux backup create --project ${{ vars.FLUX_PROJECT_SLUG }} --hash ${{ vars.FLUX_PROJECT_HASH }}    flux backup verify --project ${{ vars.FLUX_PROJECT_SLUG }} --hash ${{ vars.FLUX_PROJECT_HASH }} --latest- name: Apply migrations  run: flux push ./db/migrations/# If migration fails, the verified backup is recoverable.

Keep the API token used here narrow — read-write to the projects it touches, nothing more. See Project secrets.

Common pitfalls

SymptomLikely cause
flux backup verify consistently failsPostgres major-version mismatch between the verify image and the source; or the tenant schema was deleted between create and verify. role "t_<shortId>_ddl" does not exist (or t_<shortId>_role) means the control plane is older than the tenant-role stub — upgrade flux-web, then flux backup verify --latest on the existing artifact. Schema-only empty v2 tenants should verify — if you still see no user tables found after pg_restore, the control plane needs the empty-tenant verify policy.
List always shows "Validating backup artifact"The validator is stalled or the upload truncated; re-create and watch --verbose for the artifact size
Download writes nothing to diskForgot -o or a shell redirect on a TTY (the CLI refuses binary to terminal)
flux nuke refuses with "not restore-verified"Latest backup is in a non-restorable trust state; create + verify, or pass --skip-backup-check if you really mean to destroy without a recovery path
Old backup artifact missing on diskRetention swept it; older rows can have valid metadata but no file. Check the project's retention window in the dashboard
failed or restore_failed row disappearsThe hourly retention pass visits every non-excluded project and removes those rows and their files once a newer restore-verified backup exists. It does not delete the newest backup. A failed create (including strict offsite failure) records validation and restore verification as skipped

The full symptom-by-symptom map for backup failures lives in Troubleshooting.

Next steps