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>
69 lines
3.1 KiB
Markdown
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.
|