Backups and restore
GoFastr does not back up your database. This page is the operator's guide to
doing it yourself: what to back up, the commands for SQLite and Postgres, and
how to prove a restore works before you need it. For application-level,
schema-aware export (portability, GDPR, anti-lock-in) see
Data export & import — it complements a database backup and
does not replace one.
What to back up
- The database. Everything the framework persists lives here: entity rows,
auth sessions and tokens, queue jobs, audit log, migration state. - Uploaded files, if you use
battery/storagewith the local backend —
the directory you configured is not inside the database. S3-backed storage
is covered by your bucket's own versioning/replication. - Secrets are not backups.
GOFASTR_SECRET, auth secrets, and OAuth
credentials belong in your platform's secret store. A restored database is
useless without the same secrets, so record where they live — but keep them
out of the backup archive itself.
SQLite
Copying the .db file while the app is running can capture a torn state, and
in WAL mode it misses writes still sitting in the -wal file. Use SQLite's
own online-backup paths — both are safe while the app is up:
# Option 1: the backup API via the CLIsqlite3 /srv/myapp/app.db ".backup '/backups/app-2026-08-16.db'"# Option 2: VACUUM INTO — also compacts the copysqlite3 /srv/myapp/app.db "VACUUM INTO '/backups/app-2026-08-16.db'"
A plain file copy is fine only when the app is stopped. Either way, copy the
result off the host; a backup on the same disk as the database shares its
failure.
Postgres
pg_dump in custom format is the baseline — consistent snapshot, no downtime,
selective restore:
pg_dump --format=custom --file=/backups/app-2026-08-16.dump "$DATABASE_URL"# restore into an empty databasepg_restore --dbname="$RESTORE_URL" --no-owner /backups/app-2026-08-16.dump
A nightly dump bounds your data loss at up to a day. If that is too much, add
point-in-time recovery: WAL archiving (archive_command, or tooling like
pgBackRest/WAL-G), or your managed provider's PITR switch — most managed
Postgres offerings make this a checkbox and it is the single cheapest
durability upgrade available.
Prove the restore
An untested backup is a hope, not a plan. On a schedule (monthly is a
reasonable floor):
- Restore the latest backup into a scratch database or file.
- Run the same migration step your deploy runs (
gofastr migrate up --db-url=<scratch-dsn>— pass the scratch URL explicitly; a bare
--db-urlfalls back toDATABASE_URLand migrates the wrong
database — or nothing, if you accept on-boot auto-migrate) so the
drill exercises the real upgrade path. - Point a build of your app at it (
DATABASE_URLset to the scratch copy)
and boot it. Boot surfaces startup and schema failures; it does not
prove the restored rows and uploads are complete — that is what the
next step checks. - Spot-check what matters: row counts on your core entities, one login, one
file download if you back up uploads.
Write down the restore time. That number is your real recovery window, and it
only ever grows with the data.
Common mistakes
- Treating
ExportDataas the backup. The export is application-level
and declaration-aware — good for moving between databases or handing users
their data — but it is not point-in-time, does not capture tables the
framework does not know about, and both export and re-import take longer
than a native restore. Use the database's own tooling for disaster recovery
and Data export & import for portability. - Copying a live SQLite file with
cp. It can capture a torn state and
silently drops writes still in the-walfile. Use.backupor
VACUUM INTO(above), which are safe while the app runs.