Skip to main content

Backup strategy

Two independent layers protect the database: Neon point-in-time history for fast recoveries, and an automated nightly external pg_dump in a private Cloudflare R2 bucket for recoveries beyond Neon’s history window. A backup that has never been restored is not evidence of recoverability. Restore tests are mandatory before first customer use and on a scheduled cadence after that (see below).

Automated nightly backup

The .github/workflows/backup.yml workflow runs outside the deploy pipeline: scheduled nightly, or on demand with a tier selection. It creates the backup package locally and uploads it to the private R2 backup bucket - never the product-images bucket. Package contents (stored at muze-prod/<tier>/<UTC-timestamp>/): Encryption and access:
  • R2 encrypts every object at rest at the storage layer (AES-256) by default. The S3 API’s server-side-encryption header (SSE-S3) is not implemented by R2 and is not needed, so uploads do not pass --sse.
  • The bucket is private; the API token used is least privilege (write to the backup bucket only) and lives in GitHub secrets / the runbook, never in the dump or the repository.
  • Dumps hold employee, auth and order data - treat them as confidential, and never commit them to Git (working artefacts under backend/scripts/backups/ are gitignored).

Running it

Manual backups

All backups must use the database’s direct (unpooled) connection (DIRECT_URL). The normal DATABASE_URL uses Neon connection pooling and is for application traffic. pg_dump/pg_restore must also match the server’s PostgreSQL major version (Neon is currently 18; Homebrew installs an older default - use brew install postgresql@18 and add /opt/homebrew/opt/postgresql@18/bin to PATH first, otherwise pg_dump aborts with “server version mismatch”).

Recovery procedures

Accidental data deletion

  1. Inspect the deletion via the Neon dashboard (Time Travel / tables view) and confirm the exact point in time to recover to.
  2. If it is within Neon’s history window: open “Restore” from that point in time on a branch, verify the data, then promote the branch.
  3. If it is beyond the window: restore the latest R2 dump to an isolated branch with restore-test.sh, verify, then point the app at the branch.

Migration failure

Full Neon outage beyond the history window

  1. Suspend the application (Render dashboard) to stop writes.
  2. Pull the latest dump from the R2 backup bucket and restore it into an isolated Neon branch with restore-test.sh.
  3. Verify the branch holds the expected data (tables, row parity, prisma migration state).
  4. Promote the branch and swap the service’s DATABASE_URL (and DIRECT_URL) in Render - choose Save & Deploy, not Save-only.
  5. Resume the application and run smoke tests (login / order / entitlement).

Restore tests

Restore proofs run on an isolated Neon branch/DB - never on production.
The script restores the dump, verifies schema-qualified table counts, prisma migration state (against the manifest), and per-table row parity, then marks restoreTestedAt in the R2 manifest. Finish with application-level smoke tests (health, login, order, entitlement) against the restored target, record the duration, then securely restrict/delete the test database.

Test history

App-level smoke tests (health, login, order, entitlement) against a restored Neon branch remain part of production-launch readiness (Phase 2.10 gate).

Recovery time objectives

End-to-end data coverage