dorfteich/docs/architecture/adr/0013-versioning-and-trash.md
Claude Fable 5 0629411966 Add architecture documentation, ADRs, and operations concept
Initial deliverable of the architecture phase: 16 ADRs (stack, CRDT
collaboration, plugin sandbox, import/export, backups, CI/CD), data
model, permission model, real-time collaboration and plugin concepts,
deployment/operations/security documentation, and the milestone roadmap
that the implementation issues are derived from.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 14:36:16 +02:00

3.1 KiB

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.