diff --git a/.changeset/admin-backups.md b/.changeset/admin-backups.md
new file mode 100644
index 0000000000..d915176380
--- /dev/null
+++ b/.changeset/admin-backups.md
@@ -0,0 +1,7 @@
+---
+"emdash": minor
+"@emdash-cms/admin": minor
+"@emdash-cms/auth": patch
+---
+
+Adds a Backups page to admin settings: download a complete content backup (all content including drafts and trash, schema, taxonomies, menus, widgets, media metadata, and site settings — never user accounts or secrets) with one click, and optionally enable daily automatic backups to the site's storage bucket with configurable retention. A new `backups:manage` permission gates the feature to admins.
diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs
index 157d6f18db..c24a65dd3f 100644
--- a/docs/astro.config.mjs
+++ b/docs/astro.config.mjs
@@ -78,6 +78,7 @@ export default defineConfig({
{ label: "AI Tools", slug: "guides/ai-tools" },
{ label: "x402 Payments", slug: "guides/x402-payments" },
{ label: "Preview Mode", slug: "guides/preview" },
+ { label: "Backups", slug: "guides/backups" },
{
label: "Internationalization (i18n)",
slug: "guides/internationalization",
diff --git a/docs/src/content/docs/guides/backups.mdx b/docs/src/content/docs/guides/backups.mdx
new file mode 100644
index 0000000000..07863992a0
--- /dev/null
+++ b/docs/src/content/docs/guides/backups.mdx
@@ -0,0 +1,103 @@
+---
+title: Backups
+description: Download site backups, schedule automatic backups to storage, and restore with D1 Time Travel.
+---
+
+import { Aside, Steps } from "@astrojs/starlight/components";
+
+EmDash gives you three layers of protection for your content, from zero-config point-in-time recovery on Cloudflare to downloadable archives you keep yourself.
+
+## What's in a backup
+
+A backup contains everything needed to reconstruct your site's content:
+
+- All content entries, including drafts, scheduled posts, and trashed items
+- Collection and field definitions (your content model)
+- Taxonomies and term assignments
+- Menus, widgets, sections, and SEO settings
+- Revisions and media metadata
+- Site settings (title, tagline, display preferences)
+
+Backups deliberately **exclude**:
+
+- User accounts, sessions, passkeys, and API tokens — auth data is neither portable nor safe in a downloadable file
+- Secrets (preview signing secret, plugin configuration)
+- Media binaries — the actual files live in your storage bucket (R2, S3, or local); a backup carries their metadata so references stay intact
+
+Backups are JSON files in the same snapshot format used by EmDash's preview system, versioned with the EmDash release that created them.
+
+## One-click download
+
+Under **Settings → Backups** in the admin, the **Download backup** button generates a fresh backup and downloads it as a JSON file. Requires the admin role.
+
+This is the right tool before risky operations: bulk imports, schema changes, or major upgrades.
+
+## Automatic backups to storage
+
+If your site has a storage backend configured (R2 on Cloudflare, S3, or local storage), you can enable daily automatic backups:
+
+
+
+1. Open **Settings → Backups** in the admin.
+
+2. Toggle **Daily automatic backups** on.
+
+3. Choose how many backups to keep (1–30). Older archives are pruned automatically.
+
+4. Save. Backups run as part of EmDash's scheduled maintenance — no extra cron setup needed.
+
+
+
+Archives are stored under the `backups/` prefix in your bucket as `emdash-backup--.json`. The **Stored Backups** list in the admin lets you download or delete individual archives, and **Back up now** creates one on demand.
+
+
+
+Automatic backups piggyback on the scheduled maintenance tick (the same mechanism that powers
+scheduled publishing) — on Cloudflare this is the Worker's cron trigger, on Node the built-in
+scheduler. If your deployment has no cron trigger configured, use **Back up now** or the download
+button instead.
+
+## Point-in-time restore with D1 Time Travel
+
+If your site runs on Cloudflare D1, you already have full database point-in-time recovery — always on, no configuration:
+
+```bash
+# See the current bookmark (do this before risky operations)
+npx wrangler d1 time-travel info my-database
+
+# Restore the database to a specific timestamp
+npx wrangler d1 time-travel restore my-database --timestamp=2026-07-08T13:00:00Z
+```
+
+Time Travel keeps 30 days of history on the paid plan (7 days on free) with minute-level granularity. It restores the **entire database** — content, users, settings — which makes it the right tool for disaster recovery ("the import went wrong, take me back to an hour ago").
+
+See the [D1 Time Travel documentation](https://developers.cloudflare.com/d1/reference/time-travel/) for details.
+
+
+
+## Offsite database dumps
+
+For a complete SQL dump of the raw database (including users and auth tables), use Wrangler:
+
+```bash
+npx wrangler d1 export my-database --remote --output=backup.sql
+```
+
+On Node deployments, the database is a single SQLite file — copy it while the server is stopped, or use `sqlite3 emdash.db ".backup backup.db"` for a consistent online copy.
+
+## Restoring a backup
+
+Restoring from a backup JSON is intentionally not exposed as a one-click admin action yet — overwriting a live database deserves more friction than a button. Current options:
+
+- **Cloudflare:** use D1 Time Travel (above) for point-in-time restore.
+- **Full dumps:** import a `wrangler d1 export` dump with `npx wrangler d1 execute my-database --remote --file=backup.sql`.
+- **Backup JSON:** the format matches EmDash's snapshot format; a guided CLI restore is planned. Track [Discussion #142](https://github.com/emdash-cms/emdash/discussions/142).
diff --git a/packages/admin/src/components/Settings.tsx b/packages/admin/src/components/Settings.tsx
index 25aafb1988..ea60d4616a 100644
--- a/packages/admin/src/components/Settings.tsx
+++ b/packages/admin/src/components/Settings.tsx
@@ -9,6 +9,7 @@ import {
GlobeSimple,
Key,
Envelope,
+ DownloadSimple,
} from "@phosphor-icons/react";
import { useQuery } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
@@ -115,6 +116,12 @@ export function Settings() {
title={t`Email`}
description={t`View email provider status and send test emails`}
/>
+ }
+ title={t`Backups`}
+ description={t`Download backups and schedule automatic backups to storage`}
+ />
{/* Language */}
diff --git a/packages/admin/src/components/settings/BackupSettings.tsx b/packages/admin/src/components/settings/BackupSettings.tsx
new file mode 100644
index 0000000000..365215cdc1
--- /dev/null
+++ b/packages/admin/src/components/settings/BackupSettings.tsx
@@ -0,0 +1,294 @@
+/**
+ * Backup settings page
+ *
+ * One-click full backup download, scheduled backups to the site's storage
+ * bucket with retention, the list of stored archives, and a pointer to
+ * D1 Time Travel for point-in-time restore on Cloudflare.
+ */
+
+import { Button, Input, LinkButton, Loader, Switch, useKumoToastManager } from "@cloudflare/kumo";
+import { useLingui } from "@lingui/react/macro";
+import {
+ Archive,
+ ClockCounterClockwise,
+ CloudArrowUp,
+ DownloadSimple,
+ Trash,
+ WarningCircle,
+} from "@phosphor-icons/react";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import * as React from "react";
+
+import {
+ backupArchiveUrl,
+ BACKUP_EXPORT_URL,
+ createBackupArchive,
+ deleteBackupArchive,
+ fetchBackupOverview,
+ updateBackupSettings,
+ type BackupArchive,
+} from "../../lib/api/backups.js";
+import { ConfirmDialog } from "../ConfirmDialog.js";
+import { DialogError, getMutationError } from "../DialogError.js";
+import { BackToSettingsLink } from "./BackToSettingsLink.js";
+
+function formatBytes(bytes: number): string {
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+}
+
+export function BackupSettings() {
+ const { t, i18n } = useLingui();
+ const toastManager = useKumoToastManager();
+ const queryClient = useQueryClient();
+ const [archiveToDelete, setArchiveToDelete] = React.useState(null);
+
+ const {
+ data: overview,
+ isLoading,
+ error: fetchError,
+ } = useQuery({
+ queryKey: ["backup-overview"],
+ queryFn: fetchBackupOverview,
+ });
+
+ // Local form state seeded from the server once loaded
+ const [enabled, setEnabled] = React.useState(false);
+ const [retention, setRetention] = React.useState("7");
+ const seeded = React.useRef(false);
+ React.useEffect(() => {
+ if (overview && !seeded.current) {
+ seeded.current = true;
+ setEnabled(overview.settings.enabled);
+ setRetention(String(overview.settings.retention));
+ }
+ }, [overview]);
+
+ const saveMutation = useMutation({
+ mutationFn: () => {
+ // Clamp to the server's accepted range so out-of-range input saves
+ // the nearest valid value instead of failing validation.
+ const parsed = Number.parseInt(retention, 10);
+ const clamped = Number.isNaN(parsed) ? 7 : Math.min(30, Math.max(1, parsed));
+ return updateBackupSettings({ enabled, retention: clamped });
+ },
+ onSuccess: (settings) => {
+ setEnabled(settings.enabled);
+ setRetention(String(settings.retention));
+ void queryClient.invalidateQueries({ queryKey: ["backup-overview"] });
+ toastManager.add({ title: t`Backup settings saved`, variant: "success", timeout: 4000 });
+ },
+ onError: (error) => {
+ toastManager.add({
+ title: t`Failed to save backup settings`,
+ description: getMutationError(error) || t`An error occurred`,
+ variant: "error",
+ timeout: 5000,
+ });
+ },
+ });
+
+ const backupNowMutation = useMutation({
+ mutationFn: createBackupArchive,
+ onSuccess: (archive) => {
+ void queryClient.invalidateQueries({ queryKey: ["backup-overview"] });
+ toastManager.add({
+ title: t`Backup created: ${archive.name}`,
+ variant: "success",
+ timeout: 4000,
+ });
+ },
+ onError: (error) => {
+ toastManager.add({
+ title: t`Failed to create backup`,
+ description: getMutationError(error) || t`An error occurred`,
+ variant: "error",
+ timeout: 5000,
+ });
+ },
+ });
+
+ const deleteMutation = useMutation({
+ mutationFn: (name: string) => deleteBackupArchive(name),
+ onSuccess: () => {
+ setArchiveToDelete(null);
+ void queryClient.invalidateQueries({ queryKey: ["backup-overview"] });
+ },
+ });
+
+ if (isLoading) {
+ return (
+
+ {t`Download a complete backup of your site: all content (including drafts and trash), collections, taxonomies, menus, widgets, media metadata, and site settings. User accounts and secrets are never included.`}
+
+ {t`Download backup`}
+
+
+ {/* Scheduled backups */}
+
+
+
+
{t`Automatic Backups`}
+
+
+ {storageAvailable ? (
+
+
+ {t`Store a daily backup in your site's storage bucket. Old backups are removed automatically.`}
+
+
+
+ setRetention(e.target.value)}
+ />
+
+
+
+
+
+
+ ) : (
+
+
+
+ {t`Automatic backups need a storage backend (R2, S3, or local storage). Configure storage in your EmDash config to enable them.`}
+
+ {t`Sites on Cloudflare D1 can additionally restore the database to any minute within the last 30 days using D1 Time Travel — always on, no setup required.`}{" "}
+
+ {t`Learn more`}
+
+