Backup strategy
Two independent layers protect the database: Neon point-in-time history for fast recoveries, and an automated nightly externalpg_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
- Inspect the deletion via the Neon dashboard (Time Travel / tables view) and confirm the exact point in time to recover to.
- 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.
- 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
- Suspend the application (Render dashboard) to stop writes.
- Pull the latest dump from the R2 backup bucket and restore it into an
isolated Neon branch with
restore-test.sh. - Verify the branch holds the expected data (tables, row parity, prisma migration state).
- Promote the branch and swap the service’s
DATABASE_URL(andDIRECT_URL) in Render - choose Save & Deploy, not Save-only. - Resume the application and run smoke tests (login / order / entitlement).
Restore tests
Restore proofs run on an isolated Neon branch/DB - never on production.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).