0017. docs keeps spaces in PostgreSQL and collaborates through Yjs in Go
Status: proposed, 2026-10-18
Context
docs v1 serves xvii's own Markdown, built into the binary. v2 turns it into a documentation service for
teams: pages written in a block editor by several people at once, published as sites with versions,
languages, access rules and custom domains, and optionally fed from a repository. Three choices shape it:
where pages live, how simultaneous editing merges, and what format everything else (rendering, search, git,
AI, export) works on.
Decision
- Pages live in PostgreSQL, per space, team-owned like every other resource. A repository folder can be
a space's source, imported on push or by
xvii docs publish; pages from it are locked in the editor. Git
stays optional, so writing docs never requires a repository, and code-adjacent docs can still live next to
the code.
- Real-time editing with Yjs. The editor is Tiptap with its Yjs binding; menus and dialogs come from
shadcn/ui and
@xvii so it looks like the platform. docsd speaks the Yjs sync and awareness protocols over
a WebSocket and applies updates with a pure-Go Yjs implementation (binary-compatible with yjs 13, no CGO),
so the one-stack rule holds: no Node process on the server.
- Markdown is the interchange format. The server derives Markdown in v1's dialect from each page's Yjs
document; publications, search, git sync, export, MCP and AI all work on it, and the v1 content engine
renders it. The editor's block schema maps one to one onto the dialect, so Markdown imports become blocks
and pages round-trip.
- Readers see publications, not drafts. Publishing snapshots the whole space immutably (like a pages
deploy);
live points at one, and versions are named publications. Rolling back moves a pointer.
- xvii's documentation is a built-in, read-only space served by the same engine, so it keeps matching
the installed release.
Consequences
- docsd gains a database, a panel client at id and a wildcard host; its manifest changes accordingly.
- A dependency on a third-party Go Yjs port: pinned, covered by round-trip tests against fixtures produced by
the yjs version the panel ships, and isolated behind one package so it can be swapped.
- WebSocket collaboration needs Caddy to pass upgrades (it does) and keeps per-page rooms in memory on one
docsd; scaling out would need a shared room bus, not needed for one control plane (ADR 0002).
- The Markdown dialect becomes a compatibility surface: blocks added to the editor must have a Markdown form.