AltSql Restore Use Examples From the Cron Line to the Auditor Report
These are the recipes we’d hand to someone setting up AltSql Restore on a backup host for the first time. Each one was run as written on 31 October 2026 with the released altsql-restore 0.5.0, on a gzipped PostgreSQL dump and a SQLite file of Chinook, the public sample database of a digital music store. Paths were changed to a test folder for the runs, and the outputs below are copied from them.
A Nightly Cron Line
Run the check after the backup has landed, under the same --job name every night, with the history and the reports in fixed places:
# m h dom mon dow command (backup host; the dump lands by 02:30)
15 3 * * * /usr/local/bin/altsql-restore -q --job shop-db --history /var/lib/altsql/restore.db --report-dir /var/log/altsql-restore --signer "backup host" /backups/shop-latest.sql.gz || logger -t altsql-restore "shop-db backup check exit $?"
-q keeps the report off standard output, so cron has nothing to mail on a normal night, and the logger part records the nights that don’t pass. It runs on any exit status other than 0, and that includes 3, NOT VERIFIED, further down.
We ran the command from that line by hand. It exited with 0 after 2.7 seconds, the restore through a throwaway PostgreSQL server included, and left one report in the report folder, named shop-db-YYYYMMDD-HHMMSS-run1.txt with the date and time filled in. One file per night is what an auditor will ask to see.
Choosing the Shrink Limit
The default alert fires when a table loses more than 10% of its rows since the last good run. We deleted every 25th invoice line from Chinook, 89 of 2,240 rows, a 4% loss.
With the default, the night passed:
InvoiceLine 2,151 2,240 -4.0% 21ff067cc501bfab changed
Verdict: PASSED
With --max-shrink 2 on the same two files, it alerted and exited with 1:
Verdict: ALERT
- InvoiceLine shrank from 2,240 to 2,151 rows (-4.0%), more than the 2% allowed
Pick the limit from how the tables really behave. A table of orders that only grows can take 1%. A queue table that empties every night needs its own job, or a higher limit. Keep in mind that each run is compared with the last good run, so a table that loses 1% every night stays under any limit above 1%. The “previous” column shows the baseline the run was compared with, and a glance at it now and then catches that slow loss.
Accepting a Planned Clean-Up
When rows were removed on purpose, say so once with --accept, and put the reason in the signer so it lands in the report:
altsql-restore --job music-strict --history restore-strict.db --max-shrink 2 \
--accept --signer "dba: planned clean-up, ticket 4127" backups/night2.db
Verdict: ACCEPTED
- InvoiceLine shrank from 2,240 to 2,151 rows (-4.0%), more than the 2% allowed
Accepted by the operator (--accept): this run is the new baseline.
The next night ran without --accept, passed, and named its baseline as run 3 ... (ACCEPTED). Leave --accept out of the cron line. It belongs on one run, by hand.
Reading the History With SQL
The history is an AltSql DB file, so the altsql-db shell that comes in the same archive answers SQL on it. Every night that didn’t simply pass:
$ altsql-db restore-strict.db "SELECT job, run, verdict, backup FROM runs WHERE verdict <> 'PASSED';"
job|run|verdict|backup
music-strict|2|ALERT|night2.db
music-strict|3|ACCEPTED|night2.db
One table’s row count night by night:
$ altsql-db restore-strict.db "SELECT run, rows FROM counts WHERE job = 'music-strict' AND item = 'InvoiceLine';"
run|rows
1|2240
2|2151
3|2151
4|2151
And a summary for the monthly review:
$ altsql-db restore-strict.db "SELECT verdict, count(*) FROM runs GROUP BY verdict;"
verdict|count(*)
PASSED|2
ALERT|1
ACCEPTED|1
altsql-restore --list prints the same runs with the first 16 characters of each report’s seal.
Sealed Reports for an Auditor
Every report ends with a seal over all the lines above it. A plain SHA-256 seal shows that a report wasn’t damaged, but anyone who edits a report can work out a new one. For an auditor, seal with a key only you hold:
head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' > /etc/altsql/audit.key
chmod 600 /etc/altsql/audit.key
altsql-restore -q --job music-audit --signer "ops on call" --sign-key /etc/altsql/audit.key \
--report reports/audit-night1.txt backups/night1.db
The report then ends like this:
Signed off by: ops on call
Seal (HMAC-SHA256 of the lines above, keyed): 7397712c68aea36c0edfff563fc3c4710e34997474d1b4a85b86a34af2de4261
Checking it with the key:
$ altsql-restore --verify-report reports/audit-night1.txt --sign-key etc/audit.key
reports/audit-night1.txt: the seal matches; the report is as it was written
Then we changed one number in a copy of the report, 2,240 invoice lines to 2,420, and checked again:
$ altsql-restore --verify-report reports/audit-edited.txt --sign-key etc/audit.key
reports/audit-edited.txt: the seal does NOT match under this key; the report was changed or the key is another
The original report checked with a different key failed the same way, and both failures exit with 1. The history keeps every seal too: --list showed 7397712c68aea36c for this run, the start of the seal printed in the report. An auditor can take a report from the folder, check its seal, and compare it with the history.
An Alert in Your Chat or Pager
--webhook URL posts a short JSON summary with curl whenever a run doesn’t pass. --webhook-always posts after every run. We pointed it at a small local listener and ran the 4% night under a 2% limit. This is what arrived, with Content-Type: application/json:
{"tool":"altsql-restore","version":"0.5.0","job":"music-hook","run":2,"date":"2026-10-31 11:18:34 UTC","verdict":"ALERT","backup":"backups/night2.db","backup_sha256":"a87a98836076fdde07377a002505050122ec98c87d8f66112640ab3be7858531","reasons":["InvoiceLine shrank from 2,240 to 2,151 rows (-4.0%), more than the 2% allowed"],"items":11}
Most chat tools want their own message format, so a small relay usually sits between the two and turns verdict and reasons into a message. The report itself records where the summary went: Webhook: sent to http://127.0.0.1:18765/backups.
What NOT VERIFIED Means
PostgreSQL and MySQL dumps need a database server to restore into. Restore looks for local server programs first and Docker second. When it finds neither, it doesn’t guess. We pointed it at an empty folder for server programs and a Docker program that doesn’t exist:
Verdict: NOT VERIFIED
This backup was NOT restored, so it is not verified: no engine could run it here.
- local PostgreSQL: no local PostgreSQL server programs (initdb and postgres) were found
- Docker PostgreSQL: Docker was not found (/nonexistent/docker)
The exit status was 3. The history keeps the run as NOT VERIFIED, and it never becomes a baseline.
It isn’t a pass, because nothing was restored. The file could be complete or cut in half, and this run can’t tell which. It isn’t an alert about the backup either: the backup may be fine, and the host is what needs fixing. That’s why it has its own exit status. Treat 3 like 1 in your scheduler, as the cron line above does, and fix it by installing the server programs on the backup host or giving it Docker. A backup check that quietly reports green when it couldn’t run is how a team finds out a year later that nothing was ever tested.
Rerunning These
All of the recipes are in one script, engines/restore/results/case-studies/4-use-examples/run.sh, with the outputs it recorded. The case studies show the same tool on harder backups: five nights of Pagila on PostgreSQL, dumps a plain restore script called good, and a SQLite file copied during a write, the one backup in these tests that Restore passed when it shouldn’t have.