Skip to content
Last updated: Sep 25, 2026

Demo Environment Refresh (golden backup → Railway) ​

OwnerClassificationVersionEffectiveNext reviewStatus
Platform EngineeringInternal1.02026-09-052026-12-05Draft

Purpose. How to refresh a demo environment (typically Railway) with the current demo dataset. The demo state lives in two stores that must move together: the finance Postgres DB and the RustFS object store (attachments, remittance/invoice PDFs, extracted docs). The reproducible unit is a golden pair.

The golden pair ​

GoldenWhatFormat
DBthe whole finance DB (all schemas incl. demo.* functions, matching config, cash/invoice/journal data)finance-DEMO-GOLDEN-<desc>-<YYYYMMDD-HHMMSS>.dump — pg_dump -Fc (custom)
RustFSthe default bucket contents onlyobject-store-default-GOLDEN-<YYYYMMDD-HHMMSS>.zip — zip of the bucket's top-level prefixes

One shared DB backs all four dev clusters, so a single finance dump is the whole demo.

Create the goldens (local) ​

1. Arm the demo first so the golden captures a clean starting state (scenarios ready, engine not yet run):

sql
SELECT * FROM demo.reset_cashapp();   -- (+ other modules' reset_* as needed)

2. DB dump (from the Postgres container):

bash
docker exec blitz-db-1 pg_dump -U postgres -d finance -Fc -f /tmp/g.dump
docker cp blitz-db-1:/tmp/g.dump ~/blitz-goldens/finance-DEMO-GOLDEN-<desc>-$(date +%Y%m%d-%H%M%S).dump

3. RustFS zip — the bucket is small (~20 MB / a few hundred files); /data/temp is transient (can be 16 GB) and must be excluded, as must .rustfs.sys. Zip the contents of /data/default so the archive's top level is the bucket prefixes (closehub/, demo/, documentreceipts/, invoice/, journal/, reconciliation/, templates/, uploads/, zhttp/, …):

bash
docker cp blitz-s3:/data/default/. /tmp/g-src/          # bucket contents (excludes .rustfs.sys + temp)
# zip /tmp/g-src/* as object-store-default-GOLDEN-<ts>.zip  (host `zip`, or python zipfile if absent)

Verify the zip's top-level entries are bucket prefixes (not a wrapping default/ folder).

Restore to Railway ​

Railway has no shell/psql/docker at demo time, so the pieces move as artifacts:

  1. DB: pg_restore the .dump into Railway's finance DB (custom format → pg_restore -d finance).
  2. RustFS: unzip the bucket zip into the RustFS default bucket.
  3. Demo Control: the demo.* functions ride in the dump; use /system/demo-control to reset/verify on Railway (no shell needed). See Demo Control.
  4. Services/data-driven bits the dump does not cover: the emailprocessor service and the document-processqueue Temporal schedule are provisioned per-env — see Email & Document Ingestion.

If you apply migrations instead of copying the DB ​

When you pull main and apply schema (rather than copy a golden), run the drift ritualbefore prisma generate:

  1. bun run src/scripts/db/check-prisma-drift.ts — lists prisma fields/tables missing from the live finance DB (all finance schemas).
  2. Fix each with an idempotent manual/ migration (ADD COLUMN IF NOT EXISTS …). finbase.sql is pure CREATE TABLE IF NOT EXISTS + indexes (no DROP/INSERT/ALTER) — safe to rerun; it only creates what's missing. For a changed column on an existing table, IF NOT EXISTS won't alter it — drop that one (empty) table first, then rerun finbase.sql to recreate it with the new shape.
  3. Re-run the drift check until ✓ no drift, then prisma generate.

Known recurring trap: a prisma field added without a manual/ migration → the column is missing in the DB → 500s. The drift check catches it every time.

Constraints ​

  • Always take a fresh DB dump before a schema-mutating step (drop/rerun), even though a recent golden exists — keeps the demo golden pristine and gives a precise rollback point.
  • Never commit db-backups/ or ~/blitz-goldens/ artifacts to git.
  • The DB and RustFS goldens are a matched pair — restore both, or attachments/PDFs won't resolve for records the DB references.

Revision history ​

VersionDateAuthorChange
1.02026-09-05Platform EngineeringInitial runbook — golden pair (DB dump + RustFS bucket zip) format + create/restore, and the post-pull drift ritual.

Finaisse Internal — Confidential. Access-restricted; not for external distribution.