Runbook: Project Data Restore
Purpose: Recover a BoB-managed project’s Cloudflare D1 data after a bad migration, a bad deploy, a deleted row or a lost database — choosing the path with the least data lost (#3085, cap #3067) When to use: Rows in a project’s PROD database are wrong or gone; a migration went badly; the database itself is gone; or you are rehearsing a restore into TEST Prerequisites: The project’s
dev.jsondeclaresenvironments.prod.backupand its backup Worker is deployed (bob backup setup --apply, then the twonext:lines it prints);wranglerauthenticated to the project’s Cloudflare account (wrangler whoami); a terminal you are sitting at — a PROD restore never runs unattended Estimated time: 5–15 minutes for Time Travel; the export path’s time is whatbob backup restoreprints as its measured RTO
This runbook is about a project’s data. Rebuilding BoB itself (BOB_HOME
and a project checkout) is the other drill:
Restore Drill.
RPO and RTO — which path loses less
Section titled “RPO and RTO — which path loses less”| Path | RPO (how much data you can lose) | RTO (how long it takes) | Reaches back |
|---|---|---|---|
1. D1 Time Travel (wrangler d1 time-travel restore) |
about a minute — any point in time | minutes, in place | 30 days (Workers Paid), 7 days (Free) |
2. Nightly export (bob backup restore) |
up to one schedule interval — a day with the default 0 3 * * * |
measured: bob backup restore prints it |
the declared retention: 7 dailies, 12 monthlies by default |
Time Travel loses less and is faster, so it comes first. The export is for what Time Travel cannot reach: a point older than its window, a database that was deleted, or a restore into another database (TEST) to inspect before touching PROD.
1. Stop the damage
Section titled “1. Stop the damage”Stop whatever is still writing bad data: pause the deploy, disable the cron, or put the site into maintenance. Note the time the damage began, in UTC — every later step asks for it.
date -u +%Y-%m-%dT%H:%M:%SZ2. Take a backup of the damaged state
Section titled “2. Take a backup of the damaged state”Keep what is there now, so the restore itself can be undone:
bob backup runExpected: landed <db>/<stamp>.sql … after …. Exit 1 means the Worker did
not land a dump in time; carry on — Time Travel does not need it.
3. Path 1 — D1 Time Travel (recent mistake, database still exists)
Section titled “3. Path 1 — D1 Time Travel (recent mistake, database still exists)”Find the bookmark for a moment just before the damage began:
wrangler d1 time-travel info <database_name> --timestamp 2026-09-29T09:14:00ZIt prints the bookmark for that moment. Then restore the database in place to that bookmark:
wrangler d1 time-travel restore <database_name> --bookmark <bookmark>wrangler asks you to confirm, then reports the restore along with the bookmark the database was at before it. Keep that bookmark: restoring to it undoes this restore. Go to step 5.
If the timestamp is outside the Time Travel window, or the database was deleted, use path 2.
4. Path 2 — the nightly export (bob backup restore)
Section titled “4. Path 2 — the nightly export (bob backup restore)”Check which dumps you have and how old the newest is:
bob backup statusRestore into TEST first and verify it there. With no --from, the newest
dump is used; name an older one to reach back before the damage:
bob backup restore # the newest dump → TESTbob backup restore --from <db>/2026-09-28T0300Z.sql # a named dump → TESTA deploy that failed after its migration applied. bob deploy (and the
promote workflow in CI) takes a backup immediately before it applies D1
migrations, when the env declares a backup and migrations are pending. The
deploy output names it on one line:
pre-migrate backup: r2://<bucket>/<database>/<stamp>.sqlThat dump is the schema and data from just before the migration. Pass the
r2:// key as it is printed:
bob backup restore --from r2://<bucket>/<database>/<stamp>.sql # → TEST first, as aboveExpected: one ok line per table (users: 2 restored, 2 in the sidecar),
then restored and verified in … (…s, measured RTO). Exit 1 with DIFF
lines means the row counts did not match — do not take that dump to PROD.
Check the TEST app against it. When it is right, restore the same dump into PROD. This drops PROD’s tables and replaces them, so it is refused unless a human passes its gate: automation Level 1, or the explicit flag, and then the database name typed at the terminal:
bob backup restore --env prod --from <db>/2026-09-28T0300Z.sql --i-understand-this-overwrites-prodExpected: the prompt This overwrites the PROD database <database_name>. Type its name to go on:, then the same ok lines and the measured RTO. Exit 4
means the gate refused and nothing was touched.
For a project whose Cloudflare account an agent may not act in (Client A’s),
the operator runs this step; an agent files a todo with the exact command.
5. Verify and record
Section titled “5. Verify and record”- Load the app against PROD and check the rows that were damaged.
- Run
bob backup status— it should beFRESH. - Comment on the incident issue (or the drill’s run issue) with the path taken, the timestamp or dump restored, the measured RTO, and how much data was lost (the RPO you actually got).