# ADR 0013: Page version history via Yjs snapshots, soft-delete trash - Status: accepted - Date: 2026-07-04 ## Context Kickoff decision: version history (who changed what, view and restore old versions) and a trash (soft delete) are core product features, designed in from the start because retrofitting them into a CRDT data model is costly. Yjs (ADR 0003) stores documents as update logs with tombstones, which gives natural hooks for both. ## Decision ### Version history - **Automatic versions**: the collab server creates a version snapshot when a page's editing session ends (last participant disconnects) or after a configurable active-editing interval (default 30 min), skipping no-op periods. - **Named versions**: editors can create a version explicitly with a label ("before restructuring"). - Storage: `page_versions` rows hold a full encoded Yjs state (`bytea`), created-at, trigger (auto/manual/pre-restore), label, and the set of contributing user ids since the previous version (derived from Yjs update metadata). - **Viewing**: read-only render of any version + a text-level diff against the current version (diff computed on the derived plain/Markdown representation — good enough for "what changed", no structural diff UI in v1). - **Restore**: restoring creates a _new_ state on top of history (the restored content is applied as a regular update, preceded by an automatic "pre-restore" snapshot). History is append-only; nothing is rewritten. - **Compaction**: to bound Yjs update-log growth, the persistence layer periodically merges the update log into the current state vector; version snapshots are self-contained, so compaction never loses restorable history. Retention of auto-versions is configurable (default: keep all for 90 days, then thin to daily). ### Trash (soft delete) - Deleting a page sets `deleted_at` (+ who deleted it); the page disappears from sidebar, search, links (backlinks show a "deleted" hint), and the collab server refuses new sessions on it. - Pond Admins and the deleting editor see the pond's trash, can restore or purge. Auto-purge after a configurable retention (default 30 days) — purging deletes document state, versions, and files (ADR 0011). - Deleting a whole pond follows the same pattern at pond level (Site-Admin-visible trash, same retention). ## Consequences - Version storage costs extra database volume; snapshot thinning and compaction keep it bounded and are covered by explicit stories. - "Who changed what" is per-version granularity (contributor set), not per-keystroke attribution — deliberate simplification for v1. - The permission model needs one extra rule: viewing history/trash requires the same permission as editing the page (see `permissions.md`). ## Alternatives considered - **Event-sourcing every update forever, no compaction**: unbounded growth; rejected. - **Storing Markdown snapshots instead of Yjs states**: smaller, but restore would lose structure/plugin blocks and break the CRDT continuity; rejected (Markdown diffs are still used for the _display_ layer). - **Hard delete only**: data-loss risk contradicts kickoff decision.