Backups & migrations
Two complementary tools — full SQLite backup for data, JSON snapshot for schema sync across environments.
Vaultbase ships two distinct mechanisms for persistence portability:
- Backup & restore — full database snapshot (data + schema). Settings → Backup & restore. For disaster recovery and DB-level rollback.
- Migrations — JSON snapshot of the schema (no data). Settings → Migrations. For shipping schemas between environments.
Backup & restore (data + schema)
Section titled “Backup & restore (data + schema)”Scheduled backups
Section titled “Scheduled backups”Settings → Backup & restore turns on built-in scheduled backups:
- Schedule: a cron expression in UTC. Default
0 3 * * *(daily, 03:00). - Keep: how many snapshots to keep (default 7). Older ones are deleted after each run, on disk and in S3.
- Copy to S3: optionally upload each snapshot to an S3-compatible bucket (AWS S3, Cloudflare R2, Backblaze B2, MinIO). Its credentials are separate from file storage. Use a private bucket: a snapshot contains password hashes and every setting.
Snapshots are written to <dataDir>/backups/ as
vaultbase-YYYYMMDD-HHMMSS-mmm.db. They’re taken with SQLite VACUUM INTO,
so they’re consistent while the server keeps serving writes. In cluster mode
only one worker runs each scheduled backup.
The same page lists stored snapshots, with download, restore and delete buttons, and shows the last run, next run and last error. Back up now runs the same pipeline immediately.
Settings keys, if you prefer PATCH /api/v1/admin/settings:
| Key | Default |
|---|---|
backups.enabled | 0 |
backups.cron | 0 3 * * * |
backups.keep | 7 (1–1000) |
backups.s3.enabled | 0 |
backups.s3.endpoint / bucket / region / prefix | — / — / auto / vaultbase-backups/ |
backups.s3.access_key_id / secret_access_key | — |
HTTP API
Section titled “HTTP API”GET /api/v1/admin/backups ← stored snapshots + schedule statusPOST /api/v1/admin/backups ← back up nowGET /api/v1/admin/backups/<name> ← download a stored snapshotDELETE /api/v1/admin/backups/<name>POST /api/v1/admin/backups/<name>/restore ← restore a stored snapshotGET /api/v1/admin/backup ← download a fresh snapshot (not stored)POST /api/v1/admin/restore ← multipart upload, replaces data.dbRestore replaces all current data. The file is checked first: it must
pass SQLite’s integrity check and contain a vaultbase_collections table.
A file that fails is refused with 422 and nothing changes.
The server then stops its read workers and the WAL checkpointer (each holds
its own connection to the file), swaps the file in, and starts them again.
Reads that arrive meanwhile run on the main thread. The previous database is
kept next to it as data.db.bak.<ms>. The JWT signing key is unchanged, so
existing tokens stay valid — except sessions and API tokens revoked since the
backup was taken (individually, by “sign out everywhere”, or by a password
reset), which stay revoked. Collections, hooks, routes and jobs are reloaded
from the restored database straight away.
In cluster mode (vaultbase cluster) online restore answers 409.
Offline restore is the most robust path, and the only one in cluster mode:
stop the service, replace data.db (and remove data.db-wal and
data.db-shm), and start it again.
A database backup holds only the database. Not included:
<dataDir>/uploads/(file uploads — back up the filesystem separately)- hook, route, job and extension files kept on disk
<dataDir>/.secretand<dataDir>/.encryption-key(keys). To restore on another host, bring the encryption key too (VAULTBASE_ENCRYPTION_KEYor the file): without the original key, encrypted fields and stored secrets can’t be read. Bring the JWT secret as well if existing tokens should stay valid.- secrets and config passed through the environment or a config file
<dataDir>/logs/(request logs; deleted aftermaintenance.retention_days, default 30)
vaultbase backup CLI
Section titled “vaultbase backup CLI”The same binary can take a snapshot without going through the HTTP API. Useful for cron jobs that already have shell access to the host:
# local file (atomic VACUUM INTO — no WAL sidecars, no admin token)vaultbase backup --to ./backups/snapshot-$(date -u +%Y%m%d-%H%M%S).db
# gzip the snapshotvaultbase backup --to ./snapshot.db.gz --gzip
# straight to object storage — no local intermediatevaultbase backup --to s3://my-bucket/vaultbase/$(date -u +%F).dbvaultbase backup --to r2://my-bucket/snapshot.db # Cloudflare R2vaultbase backup --to b2://my-bucket/snapshot.db # Backblaze B2S3-family destinations read credentials from the standard env vars:
AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_REGION (default: auto)AWS_ENDPOINT_URL (required for r2:// and b2://, and for MinIO)For r2:// and b2:// you can set R2_ENDPOINT / B2_ENDPOINT instead of
AWS_ENDPOINT_URL. Credentials and the endpoint are checked before the
snapshot is taken, so a misconfigured run fails without leaving a database
copy behind.
The CLI uses SQLite VACUUM INTO so it’s safe to run against a live
database — the snapshot is internally consistent without pausing writes.
Combine with cron / systemd-timer:
0 3 * * * /usr/local/bin/vaultbase backup --to s3://acme-backups/vaultbase/$(date -u +\%F).db --quietFor uploads, snapshot the filesystem with restic / borgbackup /
rsync — they’re just files.
Schema migrations (no data)
Section titled “Schema migrations (no data)”Designed for: dev → staging → prod schema sync. Round-tripping schemas via git.
Export
Section titled “Export”Settings → Migrations → Download snapshot in the admin, or:
GET /api/v1/admin/migrations/snapshot → JSON body, downloads as vaultbase-snapshot-YYYY-MM-DD.jsonSnapshot shape:
{ "generated_at": "2026-04-27T12:34:56.000Z", "version": 1, "collections": [ { "name": "posts", "type": "base", "fields": [ { "name": "title", "type": "text", "required": true }, { "name": "body", "type": "text" } ], "list_rule": null, "view_rule": null, "create_rule": "@request.auth.id != \"\"", "update_rule": "", "delete_rule": "" }, ... ]}Drops the DB-only fields (id, created_at, updated_at) since name is
the cross-environment identifier.
A singleton collection is exported with "type": "singleton". Applying the
snapshot creates the collection together with its one record, fields empty. The
record’s values are data, so they aren’t in the snapshot.
Exports always write all five rules, public ones (null) included. In a
snapshot you write by hand:
| Rule in the snapshot | Creating the collection | Applying to an existing one (sync) |
|---|---|---|
null | Public | Set to public |
"" or an expression | That rule | Set to that rule |
| Left out | Admin only ("") | Left as it is |
So a sync never opens or closes access because a rule is missing.
Preview the diff before applying
Section titled “Preview the diff before applying”POST /api/v1/admin/migrations/diff{ "snapshot": { ...JSON snapshot... } } → { "data": { "added": [ { "name": "audits", "type": "base" } ], "modified": [ { "name": "posts", "type": "base", "changes": ["fields: 1 added", "view_rule changed"] } ], "unchanged": [ { "name": "users" } ], "removed": [ { "name": "drafts" } ] } }Returns admin-friendly change strings — how many fields would be added, removed or modified, which rules differ, and so on. Nothing is applied; this is purely read-only.
The Admin Migrations tab shows this preview before Apply runs:
chips for added / modified / unchanged / removed plus an expandable
<details> block listing the per-collection changes for everything in
modified. The Apply button stays disabled until the diff has loaded
— you can’t accidentally apply a snapshot you haven’t reviewed.
Settings → Migrations → Apply snapshot with mode selector, or:
POST /api/v1/admin/migrations/apply{ "snapshot": { ...JSON snapshot... }, "mode": "additive" // default; "sync" updates existing too}Modes:
| Mode | What it does |
|---|---|
additive (default, safe) | Creates missing collections. Skips existing ones — never modifies them. |
sync | Also updates existing collections to match the snapshot (fields, view query, and the rules the snapshot carries). Removed fields drop their column and data. Confirm before running. |
Neither mode ever deletes a collection. Drop manually if needed.
Response:
{ "data": { "created": ["posts", "users"], "updated": [], "skipped": ["existing_collection"], "errors": [] }}Recommended workflow
Section titled “Recommended workflow”- Build your schema in dev via the admin UI.
- Download snapshot → commit
schema.jsonto git. - CI / deploy script:
- Spin up prod with empty
<dataDir>(or existing). - Apply the snapshot (additive on first deploy, sync on follow-ups — gated by manual confirm).
- Spin up prod with empty
- App data flows in via the records API — no schema work in prod.
Limitations
Section titled “Limitations”- Type changes within a field are blocked (would require data conversion). Drop + re-add the field to change a type.
- Collection type changes (e.g. base → auth) are blocked in sync mode — drop the collection manually first. A collection can’t be turned into a singleton or out of one either: create a new collection and move the data.
CSV import / export (data, base collections)
Section titled “CSV import / export (data, base collections)”For bulk data, not schemas:
- Records page → Export — downloads all rows of a base collection as CSV. Excludes password fields. Object/array values JSON-encoded into a single cell.
- Records page → Import — upload a CSV; every row becomes a new record, rows go through validation, and per-row errors are returned in the response summary. See CSV import / export.
GET /api/v1/admin/export/<collection>POST /api/v1/admin/import/<collection> ← Content-Type: text/csv