From 002d5a730581aa8a070d85b6e56c90768aa768cd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 08:29:20 +0000 Subject: [PATCH] rename(studio): replace "restore points" copy with "snapshots"/"snapshot lifecycle" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Restore points" read as an invented umbrella term competing with "backup," "snapshot," and "PITR recovery point." Visible copy now says what it means: "snapshots" for the Storage-side artifact (matching the existing Snapshots tab and bucket-level toggle), "snapshot lifecycle" for the project-level frequency/retention settings, and "backup" for per-backup coverage on the Database Backups page. Internal type/hook/ file names (RestorePointPolicy, restore-points-query.ts, etc.) are left as-is, same as the earlier Trash->Deleted files rename. Also fixed a stale line in the Snapshots tab that still said snapshots expire after a hardcoded 90 days per-bucket, predating the project-level retention restructure — now reads the real policy. Updated the design docs to match, including reversing an earlier documented decision in README.md that argued for "restore points" over "lifecycle policy." --- .../Backups/RestorePoints/CoverageChips.tsx | 4 +-- .../RestorePoints/RestoreBackupModal.tsx | 4 +-- .../RestorePoints/StorageCoverageNotice.tsx | 10 +++---- .../Storage/BucketDataProtectionFields.tsx | 14 +++++----- .../Storage/Snapshots/Snapshots.tsx | 26 +++++++++++++------ .../StorageSettings/RestorePointsSettings.tsx | 25 +++++++++--------- .../restore-points/restore-points-query.ts | 3 ++- .../DEMO-SCRIPT-AND-UPDATE.md | 16 +++++++----- .../IMPLEMENTATION.md | 4 +-- design/storage-snapshots-versioning/README.md | 18 ++++++------- .../prototype.html | 2 +- 11 files changed, 70 insertions(+), 56 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Backups/RestorePoints/CoverageChips.tsx b/apps/studio/components/interfaces/Database/Backups/RestorePoints/CoverageChips.tsx index 7a17c168ff6..a122ba389be 100644 --- a/apps/studio/components/interfaces/Database/Backups/RestorePoints/CoverageChips.tsx +++ b/apps/studio/components/interfaces/Database/Backups/RestorePoints/CoverageChips.tsx @@ -4,8 +4,8 @@ import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import type { PrimitiveCoverage } from '@/data/restore-points/restore-points-mocks' /** - * Per-primitive coverage chips for a restore point. Makes it obvious at a glance - * that a database backup does not necessarily protect Storage objects. + * Per-primitive coverage chips for a backup. Makes it obvious at a glance that a + * database backup does not necessarily protect Storage objects. */ export const CoverageChips = ({ primitives }: { primitives: PrimitiveCoverage[] }) => { return ( diff --git a/apps/studio/components/interfaces/Database/Backups/RestorePoints/RestoreBackupModal.tsx b/apps/studio/components/interfaces/Database/Backups/RestorePoints/RestoreBackupModal.tsx index 6fc01497945..9b16d79aede 100644 --- a/apps/studio/components/interfaces/Database/Backups/RestorePoints/RestoreBackupModal.tsx +++ b/apps/studio/components/interfaces/Database/Backups/RestorePoints/RestoreBackupModal.tsx @@ -25,7 +25,7 @@ interface RestoreBackupModalProps { } /** - * Restore a platform restore point. + * Restore a platform backup. * * Two modes, because branching is a platform primitive: restoring into a preview * branch is a copy-on-write clone — cheap, non-destructive, and verifiable @@ -80,7 +80,7 @@ export const RestoreBackupModal = ({ >
-

What this restore point includes

+

What this backup includes

{coverage?.primitives.map((primitive) => { const isCovered = primitive.status === 'covered' diff --git a/apps/studio/components/interfaces/Database/Backups/RestorePoints/StorageCoverageNotice.tsx b/apps/studio/components/interfaces/Database/Backups/RestorePoints/StorageCoverageNotice.tsx index f6996d4b9c9..cc9308da986 100644 --- a/apps/studio/components/interfaces/Database/Backups/RestorePoints/StorageCoverageNotice.tsx +++ b/apps/studio/components/interfaces/Database/Backups/RestorePoints/StorageCoverageNotice.tsx @@ -9,7 +9,7 @@ import { useRestorePointPolicyQuery } from '@/data/restore-points/restore-points interface StorageCoverageNoticeProps { /** * Scheduled backups restore to a discrete point; PITR restores to any second, - * which storage restore points can't match — that caveat only applies to PITR. + * which storage snapshots can't match — that caveat only applies to PITR. */ mode: 'scheduled' | 'pitr' } @@ -19,7 +19,7 @@ interface StorageCoverageNoticeProps { * pages. * * Replaces the old always-on "Storage objects are not included" alert, which is - * wrong once a bucket is included in restore points. States the project's actual + * wrong once a bucket is included in snapshots. States the project's actual * coverage and links to the one place it's configured. */ export const StorageCoverageNotice = ({ mode }: StorageCoverageNoticeProps) => { @@ -52,13 +52,13 @@ export const StorageCoverageNotice = ({ mode }: StorageCoverageNoticeProps) => { const configureAction = ( ) const pitrCaveat = mode === 'pitr' - ? ' Storage restores to the nearest restore point before your chosen time, not the exact second.' + ? ' Storage restores to the nearest snapshot before your chosen time, not the exact second.' : '' if (isCaptureOff) { @@ -79,7 +79,7 @@ export const StorageCoverageNotice = ({ mode }: StorageCoverageNoticeProps) => { b.name).join(', ')} restore alongside the database. ${excluded.map((b) => b.name).join(', ')} will keep ${excluded.length === 1 ? 'its' : 'their'} current files, so restored rows may reference objects that no longer exist.${pitrCaveat}`} >
{configureAction}
diff --git a/apps/studio/components/interfaces/Storage/BucketDataProtectionFields.tsx b/apps/studio/components/interfaces/Storage/BucketDataProtectionFields.tsx index 2c1992d8aa9..6cb5fc25c3e 100644 --- a/apps/studio/components/interfaces/Storage/BucketDataProtectionFields.tsx +++ b/apps/studio/components/interfaces/Storage/BucketDataProtectionFields.tsx @@ -32,10 +32,10 @@ interface BucketDataProtectionFieldsProps { * * Deliberately scoped to what is genuinely per-bucket: versioning (recovery depth * for individual files, where churn varies bucket to bucket) and whether this - * bucket participates in the project's restore points. Restore point frequency - * and retention are project-level — a restore point only means anything if it + * bucket participates in the project's snapshots. Snapshot frequency and + * retention are project-level — a snapshot generation only means anything if it * covers every bucket the database references, so per-bucket retention would let - * older restore points quietly become partial. + * older snapshots quietly become partial. * * Prototype: manages local state and isn't persisted through the bucket mutation. */ @@ -150,7 +150,7 @@ export const BucketDataProtectionFields = ({ bucketName }: BucketDataProtectionF
- +

Capture this bucket so it can be restored alongside a database backup

@@ -171,7 +171,7 @@ export const BucketDataProtectionFields = ({ bucketName }: BucketDataProtectionF {policy.retentionDays} days.{' '} ) : ( - <>Restore points are turned off for this project. + <>Snapshot capture is turned off for this project. )} Frequency and retention are set for the whole project in{' '} @@ -184,8 +184,8 @@ export const BucketDataProtectionFields = ({ bucketName }: BucketDataProtectionF {versioning && ( )} diff --git a/apps/studio/components/interfaces/Storage/Snapshots/Snapshots.tsx b/apps/studio/components/interfaces/Storage/Snapshots/Snapshots.tsx index efd3291747e..65b693592cc 100644 --- a/apps/studio/components/interfaces/Storage/Snapshots/Snapshots.tsx +++ b/apps/studio/components/interfaces/Storage/Snapshots/Snapshots.tsx @@ -8,14 +8,16 @@ import { PageContainer } from 'ui-patterns/PageContainer' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' -import { AlertError } from '@/components/ui/AlertError' -import { usePaginatedBucketsQuery } from '@/data/storage/buckets-query' -import { useBucketSnapshotsQuery } from '@/data/storage/protection/bucket-snapshots-query' -import { type BucketSnapshot } from '@/data/storage/protection/protection-mocks' import { StorageBucketSelector } from '../StorageBucketSelector' import { RestoreSnapshotModal } from './RestoreSnapshotModal' import { SnapshotsList } from './SnapshotsList' import { TakeSnapshotModal } from './TakeSnapshotModal' +import { AlertError } from '@/components/ui/AlertError' +import { InlineLink } from '@/components/ui/InlineLink' +import { useRestorePointPolicyQuery } from '@/data/restore-points/restore-points-query' +import { usePaginatedBucketsQuery } from '@/data/storage/buckets-query' +import { useBucketSnapshotsQuery } from '@/data/storage/protection/bucket-snapshots-query' +import { type BucketSnapshot } from '@/data/storage/protection/protection-mocks' export const Snapshots = () => { const { ref } = useParams() @@ -29,6 +31,8 @@ export const Snapshots = () => { const selectedBucket = bucketParam ?? firstBucket ?? undefined + const { data: policy } = useRestorePointPolicyQuery({ projectRef: ref }) + const [showTakeSnapshot, setShowTakeSnapshot] = useState(false) const [snapshotToRestore, setSnapshotToRestore] = useState() @@ -73,7 +77,7 @@ export const Snapshots = () => { )} {isSuccess && snapshots.length > 0 && ( @@ -81,9 +85,15 @@ export const Snapshots = () => { -

- Snapshots expire after 90 days per this bucket's lifecycle policy. -

+ {policy && ( +

+ Snapshots are kept for {policy.retentionDays} days, per the project's{' '} + + snapshot lifecycle policy + + . +

+ )} )} diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/RestorePointsSettings.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/RestorePointsSettings.tsx index 4ea04e3814b..cbdf611a193 100644 --- a/apps/studio/components/interfaces/Storage/StorageSettings/RestorePointsSettings.tsx +++ b/apps/studio/components/interfaces/Storage/StorageSettings/RestorePointsSettings.tsx @@ -36,11 +36,12 @@ import { formatBytes } from '@/lib/helpers' const FREQUENCIES: SnapshotFrequency[] = ['with-database-backup', 'daily', 'hourly'] /** - * Project-level restore point policy — the canonical editor. + * Project-level snapshot lifecycle policy — the canonical editor. * - * Frequency and retention live here rather than per bucket because a restore - * point only means anything if it's consistent across every bucket the database - * references. Per bucket you choose participation, which is the cost lever. + * Frequency and retention live here rather than per bucket because a snapshot + * generation only means anything if it's consistent across every bucket the + * database references. Per bucket you choose participation, which is the cost + * lever. * * Prototype: backed by mock data, so nothing is persisted beyond the session. */ @@ -51,7 +52,7 @@ export const RestorePointsSettings = () => { const { mutate: updatePolicy, isPending: isUpdating } = useRestorePointPolicyUpdateMutation({ onSuccess: () => { - toast.success('Restore points updated') + toast.success('Snapshot lifecycle settings updated') setDraft(undefined) }, }) @@ -66,7 +67,7 @@ export const RestorePointsSettings = () => {
-

Restore points

+

Snapshot lifecycle

Captures your buckets at a point in time so Storage can be restored alongside a database backup. Applies to every bucket you include below. @@ -81,7 +82,7 @@ export const RestorePointsSettings = () => { )} {isError && ( - + )} @@ -89,7 +90,7 @@ export const RestorePointsSettings = () => { <>

- +

Snapshots the included buckets so a restore brings back files as well as data.

@@ -132,10 +133,10 @@ export const RestorePointsSettings = () => {
- +

- One retention for the whole project, so older restore points stay complete - rather than covering only some buckets. + One retention for the whole project, so older snapshots stay complete rather + than covering only some buckets.

@@ -214,7 +215,7 @@ export const RestorePointsSettings = () => { description={ includedBuckets.length === 0 ? 'Restoring a database backup will leave your files untouched, so rows may reference objects that no longer exist.' - : `${includedBuckets.length} of ${policy.buckets.length} buckets included, adding roughly ${formatBytes(includedBytes)} of retained storage per restore point.` + : `${includedBuckets.length} of ${policy.buckets.length} buckets included, adding roughly ${formatBytes(includedBytes)} of retained storage per snapshot.` } />
diff --git a/apps/studio/data/restore-points/restore-points-query.ts b/apps/studio/data/restore-points/restore-points-query.ts index c47a2e84462..7312fd8aa18 100644 --- a/apps/studio/data/restore-points/restore-points-query.ts +++ b/apps/studio/data/restore-points/restore-points-query.ts @@ -81,7 +81,8 @@ export const useRestorePointPolicyUpdateMutation = ({ await onSuccess?.(data, variables, context) }, onError(error, variables, context) { - if (onError === undefined) toast.error(`Failed to update restore points: ${error.message}`) + if (onError === undefined) + toast.error(`Failed to update snapshot lifecycle settings: ${error.message}`) else onError(error, variables, context) }, ...options, diff --git a/design/storage-snapshots-versioning/DEMO-SCRIPT-AND-UPDATE.md b/design/storage-snapshots-versioning/DEMO-SCRIPT-AND-UPDATE.md index ce3c814da9f..3caccf641eb 100644 --- a/design/storage-snapshots-versioning/DEMO-SCRIPT-AND-UPDATE.md +++ b/design/storage-snapshots-versioning/DEMO-SCRIPT-AND-UPDATE.md @@ -16,7 +16,7 @@ Setup: `pnpm dev:studio`, prototype flag on (`STORAGE_PROTECTION_ENABLED`), a pr ### Beat 1 — Enable protection (1–2 min) **Storage → Files → a bucket → Edit bucket** -- Show the **Data protection** section: object versioning switch, and "include in restore points" switch. +- Show the **Data protection** section: object versioning switch, and "include in snapshots" switch. - Point out: versioning retention shows the project default inline ("Keeping N versions for D days") with an explicit override switch to diverge per bucket; lifecycle fields use the design system's "input with unit" pattern; the cost admonition appears inline at enable-time — not buried in docs. ### Beat 2 — Versioning in the explorer (2 min) @@ -42,8 +42,8 @@ Setup: `pnpm dev:studio`, prototype flag on (`STORAGE_PROTECTION_ENABLED`), a pr - This is what changed after re-reading the vision docs. - Each backup row now shows coverage chips: **Database / Auth / Storage / Config**. - Explain: Auth users and Storage *metadata* live in Postgres, so a database restore brings them back for free. Object *bytes* don't — that asymmetry is exactly the drift bug in the PRFAQ. -- One state-aware coverage notice (not two overlapping banners anymore) names which buckets aren't covered, linking straight to the project's **Restore points** settings on the Storage Settings page — one place to fix it, not a separate dialog. -- Storage → Files → Settings → **Restore points**: frequency (with every backup / daily / hourly) and retention are project-level, since per-bucket retention would let a restore point be complete for one bucket and expired for another on the same day. Bucket-level keeps only participation (opt-out) and versioning's own override. +- One state-aware coverage notice (not two overlapping banners anymore) names which buckets aren't covered, linking straight to the project's **Snapshot lifecycle** settings on the Storage Settings page — one place to fix it, not a separate dialog. +- Storage → Files → Settings → **Snapshot lifecycle**: frequency (with every backup / daily / hourly) and retention are project-level, since per-bucket retention would let a snapshot generation be complete for one bucket and expired for another on the same day. Bucket-level keeps only participation (opt-out) and versioning's own override. - Open Restore: it defaults to **"into a new preview branch,"** not in-place. Tie this explicitly to "branching is a platform primitive, not a database feature" — a CoW branch is cheap and reversible, so verify-then-promote should be the default; destructive in-place restore becomes the deliberate exception. ### Beat 6 — Usage/billing honesty (1–2 min) @@ -57,7 +57,7 @@ Setup: `pnpm dev:studio`, prototype flag on (`STORAGE_PROTECTION_ENABLED`), a pr - **Ask the room:** - Storage: does branch-first restore hold up against how storage branching is actually sequenced (2026–2027 per the vision)? - Storage: per-bucket Deleted files, or project-wide? - - Design: does the platform reframe (coverage chips, "restore point" language) read as coherent, or is it doing too much in one screen? + - Design: does the platform reframe (coverage chips, "snapshot lifecycle" language) read as coherent, or is it doing too much in one screen? --- @@ -103,9 +103,11 @@ Setup: `pnpm dev:studio`, prototype flag on (`STORAGE_PROTECTION_ENABLED`), a pr > The existing Storage Size usage chart now splits into live objects / object versions / snapshots, folded into the section that's already there rather than bolted on as a new card, so it's consistent with how every other usage metric on that page is presented. A per-bucket table shows exactly which bucket's retention is driving the number. This was arguably the single highest-risk item in the original PRFAQ's own internal review: if a customer can't answer "when is my object truly gone, and when do I stop paying for it?" without reading docs, the feature generates billing-surprise tickets instead of trust. > > **7. Keeping Storage in sync with backups without silent drift** -> This one changed the most since I first sketched it. A single per-bucket toggle has a trap: it quietly stops covering a bucket created after the fact, so a fully-recoverable project degrades without anyone noticing — and if retention were also set per bucket, a restore point could be complete for one bucket and already expired for another on the very same day, silently. So the two settings that determine *whether a restore point exists at all* — how often Storage is captured (with every database backup, daily, or hourly) and how long it's retained — now live in one place: a project-level "Restore points" settings section, plus an "include new buckets automatically" switch so newly created buckets aren't a silent gap. +> This one changed the most since I first sketched it. A single per-bucket toggle has a trap: it quietly stops covering a bucket created after the fact, so a fully-recoverable project degrades without anyone noticing — and if retention were also set per bucket, a snapshot generation could be complete for one bucket and already expired for another on the very same day, silently. So the two settings that determine *whether a usable snapshot exists at all* — how often Storage is captured (with every database backup, daily, or hourly) and how long it's retained — now live in one place: a project-level "Snapshot lifecycle" settings section, plus an "include new buckets automatically" switch so newly created buckets aren't a silent gap. > -> Bucket-level configuration keeps only what's genuinely bucket-specific: whether a bucket participates at all (the deliberate opt-out, e.g. for a large regenerable cache bucket), and object-versioning's own retention, which can inherit the project default or be overridden per bucket — since keeping more or fewer old versions of one noisy bucket doesn't put restore points elsewhere out of sync. The old idea of a separate "sync" dialog is gone too: the coverage notice on the Backups page now links straight to this one settings page, so there's exactly one place this is configured, not two that can drift apart from each other. +> (Renamed from my earlier "restore points" language, which read as an invented umbrella term competing with "backup," "snapshot," and "PITR recovery point." Settling on "snapshots" ties it back to the Snapshots tab and the bucket-level "include in snapshots" toggle users already see.) +> +> Bucket-level configuration keeps only what's genuinely bucket-specific: whether a bucket participates at all (the deliberate opt-out, e.g. for a large regenerable cache bucket), and object-versioning's own retention, which can inherit the project default or be overridden per bucket — since keeping more or fewer old versions of one noisy bucket doesn't put other buckets' snapshots out of sync. The old idea of a separate "sync" dialog is gone too: the coverage notice on the Backups page now links straight to this one settings page, so there's exactly one place this is configured, not two that can drift apart from each other. > > --- > @@ -113,7 +115,7 @@ Setup: `pnpm dev:studio`, prototype flag on (`STORAGE_PROTECTION_ENABLED`), a pr > - Does branch-first restore hold up against how storage-level branching actually gets sequenced, or is it getting ahead of the infrastructure? > - Per-bucket "Deleted files," or one project-wide view? > - Do the coverage chips (Database / Auth / Storage / Config) communicate the right amount on one row, or is it trying to say too much at a glance? -> - Now that a restore point spans more than the database, does "Backups" still belong under Database, or does it eventually move — into a project-wide "Recovery" area, or even next to Branches once storage branching lands? +> - Now that a backup's coverage spans more than the database, does "Backups" still belong under Database, or does it eventually move — into a project-wide "Recovery" area, or even next to Branches once storage branching lands? > - Is project-level frequency + retention, plus a bucket-level participation/versioning-override split, the right amount of configuration surface — or does even two levels read as one too many for most people? > > *("Deleted files" vs "Trash" is no longer an open question — landed on "Deleted files" and it's shipped consistently across the UI.)* diff --git a/design/storage-snapshots-versioning/IMPLEMENTATION.md b/design/storage-snapshots-versioning/IMPLEMENTATION.md index 22b916dd6cc..931c63ca384 100644 --- a/design/storage-snapshots-versioning/IMPLEMENTATION.md +++ b/design/storage-snapshots-versioning/IMPLEMENTATION.md @@ -33,8 +33,8 @@ localized swap later): | Design | Where | Key files | | --- | --- | --- | -| **Data protection modal** | Create/Edit bucket → new "Data protection" section | `BucketDataProtectionFields.tsx`, wired into `CreateBucketModal.tsx` + `EditBucketModal.tsx`. Bucket-level only: versioning (with override) + restore point participation | -| **Restore point policy** | Storage → Files → **Settings** | `StorageSettings/RestorePointsSettings.tsx` — canonical project-level editor (frequency, retention, include-new-buckets, per-bucket participation) | +| **Data protection modal** | Create/Edit bucket → new "Data protection" section | `BucketDataProtectionFields.tsx`, wired into `CreateBucketModal.tsx` + `EditBucketModal.tsx`. Bucket-level only: versioning (with override) + snapshot participation ("Include in snapshots") | +| **Snapshot lifecycle policy** | Storage → Files → **Settings** | `StorageSettings/RestorePointsSettings.tsx` — canonical project-level editor (frequency, retention, include-new-buckets, per-bucket participation). Displayed as "Snapshot lifecycle"; the component/file/hook names still say "RestorePoint*" internally — only the visible copy moved away from that term | | **Snapshots (2a)** | Storage → Files → **Snapshots** tab (`/storage/files/snapshots`) | `Snapshots/Snapshots.tsx`, `SnapshotsList.tsx`, `TakeSnapshotModal.tsx`, `RestoreSnapshotModal.tsx`; page `pages/project/[ref]/storage/files/snapshots/index.tsx` (+ route) | | **Versions tab (3a)** | Storage explorer → select a file → preview pane | `StorageExplorer/VersionHistory.tsx`, tabs added to `StorageExplorer/PreviewPane.tsx` | | **Storage size breakdown (4a)** | Org → **Usage** page | `StorageRetentionUsage/StorageRetentionUsage.tsx`, mounted in `Organization/Usage/Usage.tsx` | diff --git a/design/storage-snapshots-versioning/README.md b/design/storage-snapshots-versioning/README.md index 4d4c45cba9d..4f9e71acbe8 100644 --- a/design/storage-snapshots-versioning/README.md +++ b/design/storage-snapshots-versioning/README.md @@ -150,7 +150,7 @@ This directly implements the External FAQ ("If an object belongs to a snapshot, ## 4. Deliverable 3 — Visualize / navigate / interact with snapshots -**Principle:** a snapshot is a bucket-wide, immutable restore point. It gets its own nav section (`/storage/snapshots`) built on the existing list-page pattern (`PageContainer` + search + sort + `Card`-wrapped `@tanstack/react-table`, as in `FilesBuckets/index.tsx`). +**Principle:** a snapshot is a bucket-wide, immutable point-in-time capture. It gets its own nav section (`/storage/snapshots`) built on the existing list-page pattern (`PageContainer` + search + sort + `Card`-wrapped `@tanstack/react-table`, as in `FilesBuckets/index.tsx`). ### 4.1 Snapshots list @@ -361,7 +361,7 @@ Design decisions: ## 8b. Where should Backups live in the dashboard? -Raised once restore points became environment-wide: if a backup now covers Database, Auth, Storage, and Config, is `Database → Backups` still the right home? +Raised once a backup's coverage became environment-wide: if a backup now covers Database, Auth, Storage, and Config, is `Database → Backups` still the right home? **Recommendation: keep it under Database for now, but rename the page from "Database Backups" to "Backups."** @@ -372,7 +372,7 @@ Reasons to stay: What would change our mind — two plausible end states, both post-Select: 1. **A project-level "Recovery" section**, once Storage snapshots, config-from-git, and Compute state are all first-class inputs and the page is no longer database-dominated. -2. **Absorbed into Branches.** If restoring is primarily "create a branch from a past point, verify, promote," then restore points are a *time axis on branches* and belong beside them. The vision's framing — branching as a CoW primitive that everything inherits — points here. +2. **Absorbed into Branches.** If restoring is primarily "create a branch from a past point, verify, promote," then backups and snapshots are a *time axis on branches* and belong beside them. The vision's framing — branching as a CoW primitive that everything inherits — points here. Either way the trigger is the same: revisit when storage-level branching lands (2026–2027 per the engineering vision), because that's when "restore" stops being a database operation with attachments and becomes an environment operation. @@ -398,14 +398,14 @@ And **current-object expiry is a different product** (S3 lifecycle expiration). ### Why snapshot retention can't be per-bucket -A restore point's entire value is being consistent *across* buckets, because the database references objects in all of them. If bucket A keeps snapshots 90 days and bucket B keeps 30, then on day 31 the older restore points silently degrade into **partial** ones — restorable for A, gone for B — and you find out mid-incident. +A snapshot generation's entire value is being consistent *across* buckets, because the database references objects in all of them. If bucket A keeps snapshots 90 days and bucket B keeps 30, then on day 31 the older snapshots silently degrade into **partial** ones — restorable for A, gone for B — and you find out mid-incident. So frequency and retention are project-level, single values. Per bucket you choose *participation*, which is the cost escape hatch. This makes the surface smaller, not bigger: | Knob | Level | Why | | --- | --- | --- | | Snapshot frequency | **Project** | Consistency requires coordination across buckets | -| Snapshot retention | **Project** | Per-bucket retention makes older restore points partial | +| Snapshot retention | **Project** | Per-bucket retention makes older snapshots partial | | Bucket participates | Bucket | Cost escape hatch; defaults to *in* | | Version retention (days / max count) | Bucket, with project default | No cross-bucket consistency requirement; churn varies per bucket | | ~~Current object expiry~~ | Deferred | Different product; see above | @@ -414,23 +414,23 @@ Versioning is the opposite case: undoing an overwrite in `avatars` has no relati ### Frequency defaults to inheritance, not configuration -Default is **"with every database backup."** An independent cadence actively breaks the headline value: you'd get storage restore points with no matching database backup, so you could restore the files but not the database state that referenced them. Daily/hourly remain available as advanced options, but for most users frequency isn't a knob they touch. +Default is **"with every database backup."** An independent cadence actively breaks the headline value: you'd get storage snapshots with no matching database backup, so you could restore the files but not the database state that referenced them. Daily/hourly remain available as advanced options, but for most users frequency isn't a knob they touch. ### Keeping two levels from being confusing 1. **Always show the effective value and its origin** — "Keeping 100 versions per file for 30 days (project default)" vs "(overridden for this bucket)". Never make users resolve inheritance in their heads. 2. **Lead with the promise, not the mechanism** — "You can roll Storage back to any of the last 90 days" instead of listing retention numbers. The policy is the implementation; the recovery window is what the customer is buying. -3. **"Restore points", not "lifecycle policy"** — the latter is S3 vocabulary that makes users learn our mechanism. +3. **"Snapshots" and "snapshot lifecycle", not "restore points".** Earlier drafts of this feature used "restore point" as an umbrella term — a single word for "the moment your whole environment can be rolled back to." In review that read as unclear: it's a made-up term competing with "backup," "snapshot," and "PITR recovery point," all of which already mean something specific to users. The prototype now says what it means: "snapshots" for the Storage-side artifact (matching the existing Snapshots tab and bucket-level "include in snapshots" toggle), and "snapshot lifecycle" for the project-level frequency/retention settings that govern them. The one place this term still earns its keep is the per-backup **coverage** concept (Database/Auth/Storage/Config) — that's about a backup's coverage, so it's described as such rather than reusing "restore point" as a noun. ### Where it's configured | Surface | Role | | --- | --- | -| Storage → Files → Settings | **Canonical editor** for the project-level restore point policy (frequency, retention, include-new-buckets, per-bucket participation) | +| Storage → Files → Settings | **Canonical editor** for the project-level snapshot lifecycle policy (frequency, retention, include-new-buckets, per-bucket participation) | | Bucket create/edit modal | Versioning (with override) + this bucket's participation; shows the inherited project policy read-only and links to Settings | | Database → Backups banner | States actual coverage and links to Settings — one editor, so the two can't drift | -**One banner, state-aware.** The Backups page previously had a permanent "Storage objects are not included" alert. Once restore points exist that statement is sometimes false, so it's a single notice with four states: feature off, capture off, partial coverage (warning + which buckets), full coverage. Never two banners describing the same thing. +**One banner, state-aware.** The Backups page previously had a permanent "Storage objects are not included" alert. Once snapshots exist that statement is sometimes false, so it's a single notice with four states: feature off, capture off, partial coverage (warning + which buckets), full coverage. Never two banners describing the same thing. ## 9. Open questions for the team diff --git a/design/storage-snapshots-versioning/prototype.html b/design/storage-snapshots-versioning/prototype.html index e46d58a390e..1d87445db0d 100644 --- a/design/storage-snapshots-versioning/prototype.html +++ b/design/storage-snapshots-versioning/prototype.html @@ -385,7 +385,7 @@
Storage · Snapshots

Snapshots

-

Point-in-time restore points for whole buckets. Auto-taken before each database backup, or created manually.

+

Point-in-time snapshots for whole buckets. Auto-taken before each database backup, or created manually.