Skip to content

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.json declares environments.prod.backup and its backup Worker is deployed (bob backup setup --apply, then the two next: lines it prints); wrangler authenticated 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 what bob backup restore prints 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.

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.

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.

Terminal window
date -u +%Y-%m-%dT%H:%M:%SZ

Keep what is there now, so the restore itself can be undone:

Terminal window
bob backup run

Expected: 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:

Terminal window
wrangler d1 time-travel info <database_name> --timestamp 2026-09-29T09:14:00Z

It prints the bookmark for that moment. Then restore the database in place to that bookmark:

Terminal window
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:

Terminal window
bob backup status

Restore 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:

Terminal window
bob backup restore # the newest dump → TEST
bob backup restore --from <db>/2026-09-28T0300Z.sql # a named dump → TEST

A 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>.sql

That dump is the schema and data from just before the migration. Pass the r2:// key as it is printed:

Terminal window
bob backup restore --from r2://<bucket>/<database>/<stamp>.sql # → TEST first, as above

Expected: 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:

Terminal window
bob backup restore --env prod --from <db>/2026-09-28T0300Z.sql --i-understand-this-overwrites-prod

Expected: 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.

  1. Load the app against PROD and check the rows that were damaged.
  2. Run bob backup status — it should be FRESH.
  3. 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).