Skip to content

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.

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:

KeyDefault
backups.enabled0
backups.cron0 3 * * *
backups.keep7 (1–1000)
backups.s3.enabled0
backups.s3.endpoint / bucket / region / prefix— / — / auto / vaultbase-backups/
backups.s3.access_key_id / secret_access_key—
GET /api/v1/admin/backups ← stored snapshots + schedule status
POST /api/v1/admin/backups ← back up now
GET /api/v1/admin/backups/<name> ← download a stored snapshot
DELETE /api/v1/admin/backups/<name>
POST /api/v1/admin/backups/<name>/restore ← restore a stored snapshot
GET /api/v1/admin/backup ← download a fresh snapshot (not stored)
POST /api/v1/admin/restore ← multipart upload, replaces data.db

Restore 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>/.secret and <dataDir>/.encryption-key (keys). To restore on another host, bring the encryption key too (VAULTBASE_ENCRYPTION_KEY or 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 after maintenance.retention_days, default 30)

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:

Terminal window
# 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 snapshot
vaultbase backup --to ./snapshot.db.gz --gzip
# straight to object storage — no local intermediate
vaultbase backup --to s3://my-bucket/vaultbase/$(date -u +%F).db
vaultbase backup --to r2://my-bucket/snapshot.db # Cloudflare R2
vaultbase backup --to b2://my-bucket/snapshot.db # Backblaze B2

S3-family destinations read credentials from the standard env vars:

AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
AWS_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 --quiet

For uploads, snapshot the filesystem with restic / borgbackup / rsync — they’re just files.

Designed for: dev → staging → prod schema sync. Round-tripping schemas via git.

Settings → Migrations → Download snapshot in the admin, or:

GET /api/v1/admin/migrations/snapshot
→ JSON body, downloads as vaultbase-snapshot-YYYY-MM-DD.json

Snapshot 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 snapshotCreating the collectionApplying to an existing one (sync)
nullPublicSet to public
"" or an expressionThat ruleSet to that rule
Left outAdmin only ("")Left as it is

So a sync never opens or closes access because a rule is missing.

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:

ModeWhat it does
additive (default, safe)Creates missing collections. Skips existing ones — never modifies them.
syncAlso 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": []
}
}
  1. Build your schema in dev via the admin UI.
  2. Download snapshot → commit schema.json to git.
  3. 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).
  4. App data flows in via the records API — no schema work in prod.
  • 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