The failure mode that only shows up when it's too late
A backup job that runs every night and reports "success" for a year can still be worthless the one time you need it — a silently truncated dump, a schema mismatch, a permissions error that only fails on restore, not on export. pg_dump finishing without an error tells you the export step worked. It tells you nothing about whether the file it produced can actually rebuild your data. The gap between "the job didn't crash" and "the backup works" is exactly where disaster-recovery plans fail in practice, and it's invisible until the day you're restoring for real.
What "verified" means here, concretely
// A daily scheduler runs pg_dump (custom format), then proves the dump is
// actually restorable: it restores it into a scratch database, compares the
// table count and key row counts against the live values captured at dump
// time, and drops the scratch database again. A backup that was never
// restored is a hope, not a backup — every run records whether the restore
// check passed.
Every run does four things, not two: dump, restore into a throwaway database, compare, clean up. The comparison isn't a checksum on the file — it's a row count on a fixed set of tables that matter (users, organizations, projects, tasks, columns, issues, time_entries, comments) checked against the counts captured from the live database at dump time. If the restored database has fewer rows in tasks than the live one had when the dump started, that's not a hypothetical failure mode — it's caught the same night, not discovered during an actual incident.
What each run records
type Run struct {
Trigger string // scheduled | manual
Status string // running | success | failed
File string
SizeBytes int64
TableCount int
RowCounts string // JSON snapshot at dump time
RestoreStatus string // verified | failed | pending
RestoreDetail string
StartedAt time.Time
FinishedAt *time.Time
DurationMs int64
}
RestoreStatus is a separate field from Status on purpose — a dump can succeed as a file while its restore check fails, and that distinction is exactly the information a backup log needs to be useful. A Status: success, RestoreStatus: failed row is the system telling you, explicitly, "you have a file, but I couldn't prove it works" — which is a very different thing to see in a log than silence.
Off-site, not just on the same disk
A backup that lives on the same server as the database it's protecting doesn't survive the failure mode where that server is the thing that's gone. WKFGo optionally ships the verified dump off-site over Telegram or Bale — configured through an operator bot token (BACKUP_TG_TOKEN, BACKUP_TG_CHAT_ID), not any tenant's own bot, so this is infrastructure the platform runs, not something each organization has to set up. The dump is encrypted before it leaves the server and chunked to fit the messaging platform's file-size limits; a RemoteSHA hash of the plaintext dump is recorded so a restored copy can be verified against it later.
Bale support specifically matters for Iranian deployments: Telegram's own API isn't reliably reachable from inside Iran, and BALE_API_BASE lets the same off-site pipeline run against Bale's API instead, without a second integration to build and maintain.
Retention, so old dumps don't accumulate forever
BACKUP_RETENTION_DAYS (default 14) deletes dump files past that age automatically — a backup system that never prunes anything eventually fills the disk it's supposed to be protecting you from losing.
Common mistakes
Trusting a green checkmark that only means "the export finished." That's the exact gap this system closes — check RestoreStatus, not just Status, when auditing backup health.
Running the scheduler under a DB role without CREATEDB. The restore-verification step needs to create and drop a scratch database; without that privilege, the dump still succeeds and the run is marked restore-failed with an actionable message rather than failing silently.
Assuming on-disk backups are enough. They protect against a bad migration or accidental deletion. They don't protect against losing the server itself — that's what the off-site copy is for.
FAQ
How often does the backup run?
Once daily, at a configurable local hour (BACKUP_HOUR, default 3am) — plus on-demand manual runs.
What happens to the scratch database used for verification? It's dropped immediately after the row-count comparison; it never persists and never receives traffic.
Can I disable the scheduler without losing manual backup capability?
Yes — BACKUP_ENABLED=false disables the automatic daily run; manual runs still work.