Backup
Scheduled snapshots & one-click restore
The backup block gives your service first-class data snapshots without bolting on external tooling. When enabled it mounts admin endpoints to create, list, download and (optionally) restore JSON snapshots of your tables, and can run them automatically on a cron schedule. Every endpoint (create/list/download/restore/delete) sits behind a godmin-only guard — authentication alone is not enough, since any one of them can dump or wipe the whole database.
Each snapshot is a portable JSON document, retained up to a configurable count, with noisy operational tables excluded by default and optional AES-256-GCM encryption at rest. Restore is gated behind the godmin role AND the allowRestore flag — but note allowRestore defaults to true, so restore is enabled unless you explicitly set it false. Set allowRestore: false in production to lock restore off.
Storage & format#
Where snapshots live, how many are kept, and what they contain. Defaults are production-sane — additive retention with operational tables excluded.
1{2 "backup": {3 "enabled": true,4 "basePath": "/admin/backup",5 "storagePath": "./backups",6 "format": "json",7 "maxBackups": 50,8 "allowRestore": false,9 "excludeTables": ["audit_logs", "backup_logs"],10 "encryptionKey": "BACKUP_ENC_KEY",11 "schedule": { "enabled": true, "cron": "0 2 * * *", "retentionDays": 30 }12 }13}enabledbooleanOptionalMount the backup service and its admin routes.
falsebasePathstringOptionalRoute prefix for the backup management endpoints.
"/admin/backup"storagePathstringOptionalFilesystem directory where snapshot files are written. Mount a persistent volume here in production.
"./backups"format'json'OptionalSnapshot serialization format. JSON keeps backups portable and human-inspectable.
"json"maxBackupsnumberOptionalHow many snapshots to keep. Older ones are pruned once the limit is exceeded.
50allowRestorebooleanOptionalWhether the restore endpoint is permitted to run. When false, restore attempts are rejected with 403 even for authenticated admins — a deliberate guard for production.
trueexcludeTablesstring[]OptionalTables omitted from snapshots — high-churn operational logs that would bloat backups and aren't business data.
["audit_logs","backup_logs"]encryptionKeystringOptionalOpt-in AES-256-GCM encryption at rest. The value is treated as an env var NAME — the plugin resolves process.env[encryptionKey] and falls back to the literal string if that var is unset. When set, new backups are written encrypted (MAGIC | IV | authTag | ciphertext); legacy plaintext backups still restore (auto-detected), and a wrong/missing key or tampered file fails closed. Note: the download endpoint streams the file verbatim as UTF-8, so encrypted backups are only consumable via the in-process restore path — leave this unset if you need portable downloadable JSON.
undefined (plaintext)Scheduled snapshots#
Opt into unattended backups on a cron cadence, with automatic pruning by age. Off by default — you decide when automation makes sense.
scheduleobjectOptionalCron-driven automatic backups.
enabledbooleanOptionalStart the cron scheduler on boot.
falsecronstringOptionalA cron-style expression parsed by a simple scheduler: it recognises */N in the hour field (every N hours) or the minute field (every N minutes), AND a fixed 'M H * * *' schedules the next run at that hour:minute, honoring time-of-day. The default 0 2 * * * therefore fires daily at 02:00. It is not a full cron engine (no day-of-week / day-of-month), but the minute+hour fields ARE honored as a daily calendar time.
"0 2 * * *"retentionDaysnumberOptionalDelete ALL backups (manual, scheduled, or pre_restore) older than this many days — pruning has no trigger filter. It only runs after each scheduled backup, so it takes effect solely when schedule.enabled is true.
30Under the hood — BackupService#
Each backup is a single JSON file plus a row in backup_logs that tracks its lifecycle. Knowing the shape and the safeguards explains the retention and restore behaviour.
snapshot file{ manifest, data }OptionalcreateBackup writes {schema}_{timestamp}.json to storagePath: a manifest (version, schema, per-table row counts + columns, totalRows) and the data (every included table's rows). Tables are discovered from the live schema, minus excludeTables — so in multi-tenant mode you can snapshot a specific tenant schema.
schema (request body){ schema?: string }OptionalBoth POST /admin/backup and POST /admin/backup/:id/restore accept an optional JSON body field `schema` (string) that targets a specific tenant in multi-tenant mode: when present it is resolved via the tenant registry (getSchemaContext), and an unknown schema is rejected with 404 { message: `Schema not found: <schema>` }; the resolved tenant's tables are then backed up / restored. Omit the field to target the service's default schema (the plugin's configured schemaName). Requires a tenant registry — without multi-tenancy the field is effectively ignored.
backup_logsstatus lifecycleOptionalEvery run inserts a log row that moves running → completed | failed (and later → restored), recording size, table/row counts, trigger (manual | scheduled | pre_restore), who ran it and the cron used. listBackups reads the latest 100; downloads stream the file by id.
restore safeguardsrestoreFromBackupOptionalRestore refuses unless allowRestore is true and the source backup is completed. It first takes an automatic pre_restore snapshot, then runs the whole restore inside ONE db.transaction (SET CONSTRAINTS ALL DEFERRED): per table it DELETEs all rows and re-INSERTs the file's rows in chunks of 500, and any mid-restore failure rolls everything back — so you never end up with a half-restored database, and the pre_restore snapshot makes even a successful restore reversible. One exception: a table that had zero rows in the snapshot is skipped entirely (the empty-rows check short-circuits before the DELETE), so it is NOT cleared — any rows added to that table since the backup are retained rather than wiped. A successful restore is additionally recorded in the audit_logs table via logger.audit (operation RESTORE, entityName backup_logs, capturing the acting user, the source backup id, the IP from x-forwarded-for and the user-agent) — a distinct side effect from the backup_logs status flip to 'restored', and emitted only after the restore succeeds.
retentionmaxBackups + retentionDaysOptionalAfter every backup enforceMaxBackups counts and prunes only COMPLETED backups beyond the newest maxBackups — its query filters backup_logs to status='completed', so restored (a restore flips the source backup's status to 'restored') and any other non-completed backups are excluded from both the count and the pruning, and can accumulate past maxBackups via this path. The scheduled run additionally calls cleanupExpiredBackups, which ignores status and drops anything older than retentionDays — that is what eventually removes those restored/non-completed backups. Both delete the file and its log row together.
Related sections