dorfteich/docs/architecture/adr/0013-versioning-and-trash.md
Claude Fable 5 b16d23297e Scaffold pnpm monorepo with lint, format, and test tooling
pnpm workspace with apps/web, apps/api, apps/collab, and
packages/shared; strict TypeScript base config, repo-wide ESLint (flat)
+ Prettier, Vitest per package, and root scripts lint/typecheck/test/
build. @dorfteich/shared ships a first health-response helper consumed
by apps/api to prove workspace linking. Existing markdown docs are
reformatted once by the new Prettier setup.

Closes #1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 19:06:27 +02:00

69 lines
3.1 KiB
Markdown

# 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.