Templates / Backup trigger
Triggering a nightly backup, and knowing it ran
The hard part of backups is not taking them. It is noticing the month they stopped — which, by construction, you find out about on the day you need one.
The recipe
- Request
- POST https://{{domain}}/api/admin/backup
- Schedule
- At 03:00 AM (0 3 * * *)
- Headers
- Authorization: Bearer {{secret}}encrypted
- Timeout
- 120s
- Attempts
- 2
- Counts as success
- 2xx
You supply
- Your domain
- example.com
Host only — no https:// and no trailing slash.
- Shared secret
- s3cr3t-valuestored encrypted
Whatever your endpoint checks the caller against.
Why this job exists
Most backup arrangements are fire-and-forget: a cron entry on the database host, a provider’s built-in snapshot, a script somebody wrote. They work until something changes underneath them — a credential rotates, a disk fills, a host is rebuilt without the crontab — and then they stop, quietly, because a cron job that fails writes to a log nobody reads.
The gap is not the backup. It is the absence of a signal. A backup system with no alerting is a system whose state you learn during an incident, and there is no worse time to learn it.
Triggering the backup over HTTP moves the trigger somewhere that keeps history and alerts. Every run is recorded with its status, duration and response, and a failure sends mail the first time it happens.
Setting it up
Have the endpoint start the backup and return only when it knows the outcome, or return a job id immediately and check it separately. What does not work is returning 202 the instant the work is queued and never looking again — that reports the request was accepted, not that a backup exists:
Reporting something worth recording
export async function POST(request: Request) {
if (!authorized(request)) return new Response('Forbidden', { status: 403 });
const result = await runBackup();
if (!result.ok) {
return Response.json({ error: result.error }, { status: 500 });
}
return Response.json({
key: result.key,
bytes: result.bytes,
durationMs: result.durationMs,
});
}Returning the size matters more than it looks. A backup that succeeds and is four kilobytes is a failed backup that returned 200, and the byte count in the run history is where you will see it.
Checking it actually works
- Restore one. An untested backup is a hypothesis; put a restore in the calendar quarterly.
- Watch the reported size over time — a sudden drop is the signal that matters, and a plateau on a growing dataset means something stopped being included.
- Confirm the retention policy on the destination bucket, so that succeeding every night does not quietly become a storage bill.
Where it goes wrong
Returning before the work finishes
A 202 for a queued backup makes this job a test of whether your web server is up. Either block until the outcome is known, or have a second job check that last night’s backup exists and is the right size.
Retrying a partial backup
This recipe allows two attempts deliberately. A backup that failed halfway generally needs looking at, not repeating — and repeating a heavy job on a struggling database is how a backup causes the outage.
Backing up to the same account you are protecting against
A snapshot in the same cloud account as the database survives disk failure and not much else — not a compromised key, not a mistaken account deletion. Cross-account or cross-provider is the difference between a backup and a copy.
Questions
Why 03:00?
It is the conventional quiet hour, and the job runs in the timezone you set rather than UTC, so 03:00 means 03:00 where your traffic is. If your users are spread across the world, pick the trough in your own traffic graph instead of a convention.
The backup takes longer than the timeout.
Then invert it: have the endpoint start the job and return an id, and add a second scheduled job that verifies the previous night’s artefact exists. Long-running work does not belong inside a single HTTP request.
Can this replace my provider’s automated backups?
It should sit alongside them. Provider snapshots are cheap and fast to restore; your own export is what survives losing access to the provider.
Running it here
This is a preset in the product, not an illustration. Pick it in the dashboard, fill in the 2 values above, and the job is created, scheduled and enabled — with retries, a record of every run showing status, latency and response, and an alert the first time one fails. Secret values are encrypted at rest and never rendered back.
The free plan runs 3 jobs hourly and needs no card. Where a recipe wants a finer cadence than your plan allows, it is slowed to the fastest schedule you are permitted rather than rejected.