backup
See also: Architecture overview · area_maintenance · db · Diffusion
src/core/area_maintenance/backup.ts dumps and lists Dédalo's PostgreSQL work
database. It is a stateless module of exported functions, driven by the
make_backup maintenance widget.
Scope
This module backs up one database with one method: a custom-format pg_dump
of the PostgreSQL work database, into the server's own backup directory.
What backup does NOT cover
It does not back up the publication database, uploaded media files,
configuration, or source code. The restore path is the sibling module
src/core/area_maintenance/restore_door.ts, driven from the shell by
bun scripts/restore.ts with the engine stopped — never an in-app action,
because the running engine cannot swap the database its own pool holds.
See How do I backup and restore.
The publication database (MariaDB) belongs to the diffusion engine; this server never connects to it. The widget's MySQL file list is therefore always empty, by design.
Where backups land
| target | function | tool | output |
|---|---|---|---|
| PostgreSQL work DB | initBackupSequence() |
pg_dump -F c -b |
<date>.<db>.postgresql_<user>[_forced]_dbv<ver>.custom.backup, plus a sibling .log capturing the dump's stderr |
getBackupDir() resolves the directory: the DEDALO_BACKUP_DIR config override
if set, otherwise <privateDir>/backups/db.
The directory derives from privateDir, never from the working directory
It is derived from the same privateDir constant the session store and the
.env loader use. An earlier cwd-based derivation meant the backup directory
silently changed depending on where the server was launched from — which is
exactly how a backup ends up somewhere nobody looks.
Version-matched pg_dump
A pg_dump client older than the server refuses to dump at all. That is
not a theoretical hazard: it silently produces zero-byte files while the calling
process reports success.
resolvePgDump() guards against it: it probes the version-suffixed installs
(postgresql@18 down to @15) newest-first before falling back to a bare
pg_dump on PATH. config.ops.pgBinPath overrides the probe.
Failure is surfaced, not swallowed
The dump runs detached (Bun.spawn + child.unref()), so
initBackupSequence() returns the pid and file path immediately rather than
blocking on a multi-gigabyte dump. That makes reporting failure the hard part, and
the module does three things about it:
- The password is threaded from
config.db.password, so a password-auth Postgres does not fail with an authentication error into a log file nobody reads while the widget reports success. - A short fast-fail window catches an immediate exit — an authentication or
connection error — and reports it as a failure, with the tail of the
.login the widget's message. - The completion check verifies a non-empty artifact. On failure it logs the
.logtail and deletes the empty file, so the backup list can never offer a zero-byte "backup" as restorable.
The widget feeds the returned pid and log path into the process-status stream, so an operator watches the dump run and sees the failure tail live.
Naming and the throttle window
initBackupSequence(userId, skipTimeRange, overrides?):
skipTimeRange = true(forced — the maintenance widget's path): second-resolutionY-m-d_Hisnaming with a_forcedmarker, no throttle check.skipTimeRange = false: hour-resolutionY-m-d_Hnaming. If the newest existing.backupfile is younger thanconfig.ops.backupTimeRangeHours(DEDALO_BACKUP_TIME_RANGE, default 8), the call returnsresult: truewith a "skipped, a recent backup already exists" message instead of dumping.
The surface
src/core/area_maintenance/backup.ts:
| function | purpose |
|---|---|
initBackupSequence(userId, skipTimeRange=true, overrides?) |
Create the backup directory if missing, apply the throttle window unless forced, build the dated filename, and spawn pg_dump -F c -b -f <path> … detached. Returns {result, msg, errors, pid?, file_path?, pfile?}. |
getBackupFiles() |
Read the backup directory and return [{name, size}] for every .backup file, newest first, with a human-readable size. Returns [] when the directory does not exist. |
newestBackupMtimeMs(backupDir?) |
The newest .backup file's mtime (0 when there are none). The recency primitive behind the throttle window and the update preconditions' "a recent backup exists" check. |
getBackupDir() |
Resolve the backup directory. |
resolvePgDump() |
Resolve the pg_dump binary path. |
getCurrentDataVersion() |
Read matrix_updates for the highest dedalo_version, parsed into [major, minor, patch]. [] on a fresh database. |
How it fits with the rest of Dédalo
- The
make_backupwidget (src/core/area_maintenance/widgets/make_backup.ts) is the only caller. It registers two actions —make_psql_backupandget_dedalo_backup_files— plus agetValuethat reports the would-be filename and the backup directory. See area_maintenance. - The update preconditions read
newestBackupMtimeMs()to warn before a destructive operation runs without a recent backup. - The restore door (
restore_door.ts) reusesverifyBackupArtifactfor its first phase — the full-read proof that an artifact is a restore point — andgetBackupDir()for its journal directory (<backup dir>/restores/). It is CLI-only (scripts/restore.ts): verify, refuse writers, one-transaction restore into a sidecar database, rename swap, the post-restore reconcile plan (src/core/reconcile/post_restore.ts), journal. - Diffusion is not involved: MariaDB belongs to the diffusion engine. See Diffusion.
Examples
Force an immediate backup
import { initBackupSequence } from './backup.ts';
// what the make_backup widget's make_psql_backup action does
const response = await initBackupSequence(-1, true); // forced → dump now, '_forced' filename
// response.result, response.pid, response.file_path
List existing backups
import { getBackupFiles } from './backup.ts';
const files = getBackupFiles(); // [{name, size}, …] — *.backup, newest first
Resolve the version-matched binary
import { resolvePgDump } from './backup.ts';
const pgDump = resolvePgDump();
// e.g. '/opt/homebrew/opt/postgresql@18/bin/pg_dump' when the server is v18
// and a matching install exists, else a bare 'pg_dump' from PATH
Related
- Architecture overview — the work-PostgreSQL vs publication-MariaDB split this module only handles one side of.
- area_maintenance — the widget that drives it.
- db — the database layer it dumps.
- Sections — the
matrixtables inside the dump.