Backups

Scheduled database backups to your private backups bucket, one-off dumps, downloads, and how to restore.

#Backups

EmuView backs up your databases into a private backups bucket on your own Cloudflare account — automatically on a schedule, or on demand from the dashboard. Restores are deliberate, CLI-driven operations.

#How it works

Each backup run dumps every database (the core database and, by default, all pool databases) using Cloudflare's D1 export, compresses the SQL, and stores it in the dedicated <instance>-backups bucket:

backups/d1/<database>/<timestamp>.sql.gz    the compressed dump
backups/d1/<database>/<timestamp>.json      a manifest (size, time, app version, status)

Two properties matter for disaster recovery:

  • The bucket is bound, not configured — it needs no credentials and no database row, so backups stay reachable even if your core database is lost.
  • The manifests are the catalog — everything needed to find and understand your backups lives in the bucket itself. With nothing but your Cloudflare account you can run npx wrangler r2 object list <instance>-backups and see them all.

The backups bucket is created by setup on new instances; existing instances get it the next time you run npm run update.

#Scheduling and retention

Configure in Settings → Backups:

Setting What it does
Scheduled Backups Turns the schedule on (off by default)
Schedule + Run Hour Daily or weekly (Sundays), at your chosen UTC hour
Include Pool Databases Back up collection data pools too (leave on)
Backups To Keep / Maximum Age Retention — older backups are pruned after each run

The newest backup per database is never pruned, so a stalled schedule can't age away your last copy.

#Confirming the schedule is actually running

Turning the schedule on is not the same as the schedule running. Scheduled backups are taken by the hourly maintenance tick — the job checks each hour whether the current UTC hour matches your configured run hour. If that tick stops, no scheduled backup is taken, and the backups list looks exactly the same as it did before, just without new entries.

Two ways to check, and it is worth checking once after you first enable the schedule:

  • The backups list — the Trigger column distinguishes cron from manual. A list with no cron entries at all, on an instance where the schedule has been on for more than a day, means the schedule has never fired.
  • System → Monitor → Health — the Scheduled maintenance panel reports the backups task directly: when it last succeeded, when it last ran, and its error if it is failing. System → Backups raises a warning banner of its own when the tick has gone stale, and a separate one when the tick is fine but the backups task has dropped out of it — that case shows no error anywhere, because the task is not running to produce one.

If either points at a problem, see Scheduled tasks aren't running.

#Taking and downloading backups

System → Backups lists every backup (newest first, with size, trigger, and the app version that took it), lets you Back up now, and downloads any dump. Failed dumps appear in the list too, with their error — gaps are visible, never silent. All of it requires the system/backups permission and is audit-logged.

Download a fresh dump (on the same page) exports one database right now and streams it straight to your browser as gzipped SQL — nothing is stored, so it works even before scheduled backups are provisioned. Handy for a quick manual grab or a one-off copy before risky work. It needs only the Cloudflare API credentials (set at setup), not the backups bucket; for a large database the download starts once Cloudflare has finished preparing the export.

#Point-in-time recovery (Time Travel)

Alongside the scheduled dumps, System → Backups shows a Time Travel panel. Cloudflare keeps a continuous history of every database — 30 days on Workers Paid, 7 days on Free — so you can restore to any instant in that window, with no scheduled backup required. It's the fast "undo the last hour" lever. It is not an off-Cloudflare copy, and neither are the R2 dumps: see Off-account copies below.

The panel lists each database's current bookmark and restorable-since date. A super admin can restore a database to a chosen time from here:

  • Restoring rewinds the entire database to that instant — every collection in it, and for the core database, users and settings too. Records written after that point are lost.
  • It's guarded: super-admin only, and you must type the database name to confirm.
  • A database that more than one Project has data on cannot be restored at all. Most databases here are shared pool shards, and a restore has no way to stop at one Project's rows: it would discard every neighbouring Project's writes back to the same instant, and none of them was asked. The restore is refused with a 409 naming how many Projects are on the database. To rewind one Project, take and restore a Project Snapshot — the instrument that can be scoped to a single Project. Whether a database is shared is read from the database registry each time you ask, not from its name, so a shard that gains a second Project stops being restorable the moment it does.
  • Cloudflare returns an undo point (the state right before the restore), shown after it completes — restore to that to reverse a mistake.
  • Cached sessions are cleared afterwards so identities re-verify against the restored data. Nobody is signed out.
  • Dangling file references are counted and reported after a core-database restore. R2 is not rewound with the database, so a restore can bring back rows whose objects are gone; the result names how many of the file references it sampled point at an object that is not there, and lists a few of them. It is a sample, not a sweep — a bucket here can hold hundreds of thousands of objects — so "nothing dangling" means nothing dangling in the sample. A pool restore reports nothing, because file records live in the core database only.

Time Travel lives inside Cloudflare, so it protects against bad data and bad migrations — not against losing the account. Neither do the R2 dumps, which sit in the same account: only an off-account copy covers that.

#Restoring from a dump

To restore from an R2 dump (e.g. recovering onto a fresh instance, or rolling back further than the Time Travel window), it's a two-step CLI operation — deliberately not a button:

# 1. Restore the dump into the database
gunzip backup.sql.gz
npx wrangler d1 execute <database-name> --remote --file backup.sql

# 2. Roll the schema forward to match the running code
npm run update -- --name <instance> --migrate-only

Step 2 matters: the dump includes the migration-tracking table, so after restoring an older backup, pending migrations re-apply cleanly on top.

#What backups do not cover

Uploaded files are not backed up. The dumps cover your databases; a file uploaded to a record exists as a single object in the files bucket and is copied nowhere. Restoring a dump onto a fresh instance gives you records whose attachments are missing. The same applies to the access-log archive, which lives in that bucket too.

That gap is now reported rather than silent on the Time Travel path (see above): a restore counts the file references it left pointing at nothing. A restore performed with the CLI steps above does not run that check — nothing in the product observes it — so after one, expect every file reference to be dangling until the objects are back in the bucket.

A Project Snapshot (GET /api/v1/system/snapshot) is a portability artefact, not a backup of a Project: it carries collection schemas and records, capped at 10,000 records per collection, and no users, roles, settings, flows or files.

Closing both gaps is planned — see ADR 0029.

#Off-account copies

Backups in the same Cloudflare account don't protect against losing the account itself. For a true off-site copy, periodically pull dumps elsewhere — download from the Backups page, or sync the bucket externally with rclone.

#The export/import API

Separate from backups, a JSON export/import exists for data portability (permission: system/backups): GET /api/v1/system/export returns core-database collection schemas + records as JSON; POST /api/v1/system/import restores that payload.

Warning

Import recreates each collection table it restores — it replaces, never merges. It covers core-database collections only; real backup and restore is the dump mechanism above.

#What you learned

  • Scheduled backups dump every database into your private backups bucket, with self-describing manifests
  • Retention prunes automatically but never deletes the newest backup
  • Time Travel gives 30 days of point-in-time restore from the dashboard (super-admin, confirmation-guarded, with an undo point)
  • Restore from a dump = load with Wrangler, then run migrations forward
  • Keep an off-account copy for account-level disasters; use export/import only for portability