Backup & Restore
What state an ObjectStack deployment actually holds, how to back up each piece per database driver, and the restore drill to rehearse before you need it.
The go-live checklist requires a documented, tested backup/restore drill. This page is that drill: what to back up, how, and how to prove the restore works.
What state a deployment holds
An ObjectStack app is deliberately easy to back up because almost everything lives in one of four places:
| State | Where it lives | Backup strategy |
|---|---|---|
Business data (records, users, orgs, runtime-authored metadata, encrypted sys_secret values) | The database behind OS_DATABASE_URL | Database-native backup — see below |
| Authored metadata (objects, views, flows, …) | Your project source + the compiled artifact | Git. The artifact is rebuildable with os build; no runtime backup needed |
Secrets (OS_SECRET_KEY, OS_AUTH_SECRET, DB credentials) | Your secret manager | Escrow the values themselves — see the warning below |
The ObjectStack home dir (~/.objectstack or <cwd>/.objectstack: default SQLite DB under data/, uploaded files under data/uploads/, dev-minted keys) | Local filesystem | Include in file backups on single-host deployments; empty on stateless containers that set OS_DATABASE_URL + OS_SECRET_KEY explicitly |
A database backup without OS_SECRET_KEY is an incomplete backup. Every
sys_secret value (encrypted settings, secret fields, datasource
credentials) is encrypted under that one key. Restoring the database on a new
host without the original key leaves those values permanently undecryptable.
Escrow the key in your secret manager the day you generate it, and treat
key + database as one backup unit.
Backing up the database
Use the native tooling of whatever OS_DATABASE_URL points at — ObjectStack
adds no extra backup surface on top of the database.
PostgreSQL
# Logical backup (small/medium databases; restore with pg_restore)
pg_dump --format=custom "$OS_DATABASE_URL" > backup-$(date +%F).dump
# Larger deployments: continuous archiving / point-in-time recovery
# (WAL archiving, or your provider's managed snapshots)SQLite (single host)
The database is a single file — but never copy it while the server is writing. Use SQLite's online backup instead:
sqlite3 /srv/data/app.db ".backup '/backups/app-$(date +%F).db'"The default location when OS_DATABASE_URL is unset is
<home>/data/objectstack.db under the ObjectStack home directory.
ObjectStack keeps file-backed SQLite in
WAL mode,
so app.db-wal beside the database can hold committed transactions that are not
yet in app.db. .backup accounts for that; copying app.db on its own does
not. If you must copy files, stop the server first (a clean shutdown checkpoints
and removes -wal/-shm), or copy app.db, app.db-wal and app.db-shm
together — a database restored beside a stale log is worse than one restored
without it.
Managed databases (hosted Postgres, MongoDB Atlas, …)
Use the provider's snapshot, branching, or point-in-time-restore facilities. Nothing ObjectStack-specific applies. (Turso/libSQL applies only to ObjectStack Cloud-managed environments — the open framework does not ship that driver.)
Backing up uploaded files
The local storage adapter keeps uploads under OS_STORAGE_LOCAL_ROOT (default
./.objectstack/data/uploads). On single-host deployments, include that
directory in the same schedule as the database so records and their
attachments restore to the same point in time. Deployments using an external
storage service (S3-compatible, etc.) inherit that service's durability and
versioning instead.
Verify the directory you are backing up actually holds the files. This
variable was previously named OS_STORAGE_ROOT, and on releases before the
rename the CLI and the settings service spelled it differently — so any value
other than the default was discarded at startup and uploads landed in
./.objectstack/data/uploads instead of where the variable pointed. A backup
job aimed at the configured path copied an empty directory. The old name
still works for one release (with a startup warning), but it now resolves to
the same place the adapter writes. Confirm with Setup → Settings → File
Storage → Root directory: it shows the path in effect, and shows it as
locked-by-env when the variable is set. If they disagree, trust the Setup page
and back that path up too.
The restore drill
Rehearse this on a scratch host before go-live, and again after any storage change. A backup you haven't restored is a hope, not a backup.
Provision a clean host
Fresh container or VM. Install Node 22+ and @objectstack/cli.
Restore the database
pg_restore --clean --if-exists -d "$OS_DATABASE_URL" backup-2026-07-14.dump
# or restore the provider snapshot
# SQLite: put the `.backup` file in place with the server stopped, and make sure
# no stale app.db-wal / app.db-shm is left beside it (see the warning above)Provide the original secrets
Set OS_SECRET_KEY and OS_AUTH_SECRET to the escrowed originals — not
freshly generated values. Restore OS_STORAGE_LOCAL_ROOT contents if you use
local file storage.
Boot from the artifact and verify
OS_ARTIFACT_PATH=./dist/objectstack.json OS_PORT=8080 os start
curl -fsS http://localhost:8080/api/v1/health
curl -fsS http://localhost:8080/api/v1/readyThen prove the restore end-to-end: sign in with a real account, open a record
that existed before the backup, and read a value that lives in an encrypted
setting (which exercises OS_SECRET_KEY).
Retention is not backup
The platform's lifecycle declarations bound telemetry data (activity 14d,
job runs 30d, notifications 90d, audit 90d-hot) — deleted-by-policy data is
gone from backups taken afterwards. If compliance needs longer, extend
lifecycle.retention_overrides before go-live, and register an archive
datasource if audit data must move to cold storage instead of being retained
hot. See Production Readiness.