docs
Documentation for teams, the way pages hosts sites: a team creates spaces (a product's docs, an
internal wiki, an API reference), writes pages in a block editor together in real time, publishes them to a
site at <space>.docs.<d> or its own domain, with versions, languages, access rules, comments, an AI
assistant and OpenAPI references. xvii's own documentation is the first space, read-only and built into the
binary. Host: docs.<d>, wildcard *.docs.<d>. Binary: docsd. v1 (stage 11) is the built-in site; v2 is
this spec (ADR 0017).
Concepts
- Space: belongs to a team (and optionally a project). Slug (DNS label, unique per instance: its host is
<slug>.docs.<d>), name, description, icon, languages (the first is the default), access, look,
navigation, custom domains, git source. A space has a draft (what editors work on) and
publications (what readers see).
- Page: belongs to a space; a tree (parent, position), slug (unique among siblings), title, icon,
description, hidden flag, per-language content. A page with children is also a section.
- Content: a Yjs document per page and language (a
Y.XmlFragment holding the editor's blocks). The
server keeps it as an update log compacted into a state snapshot, and derives Markdown from it (the same
dialect as v1: front matter, callouts, tabs, steps, cards, fenced code with titles) for rendering, search,
git, export and AI. The block schema and the Markdown dialect map one to one, so a page can round-trip.
- Publication: an immutable snapshot of the whole space (every page's Markdown, rendered HTML, the
navigation and search index) with a number, author, message and time. The space's live pointer names
one; publishing moves it, rolling back moves it back. Readers only ever see publications. The rendered
site is stored with the publication, so an engine upgrade never changes an older version; its
languages are frozen with it.
- Version: a named publication branch for an older product release (
v1, v2). The space has one
current version, edited in the draft; other versions are frozen publications, editable only by
making them current again ("restore as draft"). Readers switch versions in the site header.
- Language: each page may have content in every space language; a missing translation falls back to the
default language with a notice, as in v1. The editor shows per-page translation status.
- Access (reading) per space:
public (anyone, indexed), link (anyone with the secret link or a
password; noindex), instance (signed-in users of the instance), team (members of the owning team).
Single pages can be shared by a secret link even inside a non-public space (read-only, revocable, with
an optional expiry and password). Password attempts are limited per visitor (an IPv6 /64 is one) and
per space.
- Editing: team developers and up edit drafts and publish; team admins manage access, domains,
versions and deletion; viewers read and comment. Guest editors: instance users invited to one space
(editor or commenter) without joining the team.
- Comment thread: anchored to a text range in a page (a Yjs relative position, so it follows edits),
with replies, @mentions (notify event to the mentioned person), resolve and reopen. Visible to editors
and commenters, never on the public site.
- Template: a page or a whole space to start from. Built-in ones: product docs (Get started, Guides,
Reference, Changelog), team wiki, API reference, runbook, changelog page, meeting notes; a team can save
its own from any page or space.
- API reference: a page bound to an OpenAPI 3.x document (uploaded, or a URL fetched on publish, or a
file in the git source). Rendered as an endpoint list with parameters, schemas, examples in curl,
JavaScript, Python and Go, and a "try it" console (calls go straight from the reader's browser).
- Git source (optional, per space): a repository folder of Markdown (GitLab or GitHub, through an id
trust rule or a deploy token). A push (webhook) or
xvii docs publish imports it; one sync runs per
space at a time, and a push or Sync now during it makes it run once more when it ends. Pages that
come from the repository are marked as such and locked in the editor (edit in the repository); other
pages of the space stay editable. Exporting a space writes the same layout, so moving a space into a
repository is export, commit, connect. Disconnecting the repository stops its webhook and keeps its pages
locked until an admin makes them editable (Make editable, POST /spaces/{id}/git/unlock).
Editor
- Tiptap (ProseMirror) with Yjs collaboration; every menu, toolbar, popover and dialog from shadcn/ui and
@xvii. Blocks: paragraph, headings 1–4, lists and task lists, quote, callout (five kinds), code (language,
title, highlighted), tabs, steps, cards, table, image and file (uploaded to the space, resized, alt text),
divider, embed (video and well-known URLs), API reference, page link, math later.
/ opens the block menu; Markdown shortcuts while typing (## , - , ```, > [!NOTE]); a floating
toolbar on selection (marks, link, comment, AI); drag handles to move blocks; paste Markdown or HTML and
get blocks.
- Real time: one WebSocket per open page (
/api/v1/collab/{page}.{lang}; one person may hold 8 on a page
and 32 in all), the Yjs sync and awareness protocols, presence (avatars of who is on the page, their
cursors and selections in their colour).
docsd applies updates with a pure-Go Yjs implementation (github.com/reearth/ygo), merges them into the
stored state a moment after the last edit, and derives Markdown then. Offline edits merge on reconnect;
a replaced draft (restore, import, sync) is written over the previous document, so stale tabs and offline
caches merge into it instead of duplicating it. A block outside the schema is removed from the room
instead of stopping it from saving. Before publishing, the editor sends its whole state to
POST /pages/{id}/sync and waits for it to be saved.
- History: every page keeps snapshots (on publish, on idle after edits, at most every few minutes while
editing) with authors; any snapshot can be previewed, diffed and restored (restoring is a new edit).
- The sidebar edits the tree: add, rename, drag to reorder and nest, duplicate, move to another space,
hide, delete (to a trash kept 30 days).
Publishing
- Publish (developers): shows what changed since the live publication, page by page with diffs, takes a
message, builds the snapshot (Markdown → HTML through the v1 content engine, navigation, search index,
broken-link check that warns) and moves
live. Roll back moves live to an earlier publication.
- Versions: "Freeze as version" turns the live publication into a named version; the version menu on
the site lists them, newest first, with the current one marked "latest".
- Scheduled publishing later.
Reading site
https://<slug>.docs.<d>/<lang>/<version>/<path> (the default language and the current version may be
left out), or the space's own domains through domains (docs attachments: settings open domains'
Attach dialog for the space; domains calls /api/v1/hosts with its service token). An address that
names no space gets a page, not an API error; a closed space's access screen names nothing of it. The v1 site engine: server
rendering, search, Markdown sources (.md), llms.txt, sitemap, hreflang, per-space MCP tools.
- Look per space: name, logo, favicon, accent, background (glass like the panels), header links, footer
links and text, an optional landing page made of blocks (hero, cards): the home page shown as a hero
with the name and description, a way in, then its own blocks. Defaults to the instance's look.
No xvii service switcher on public sites; signed-in editors get an Edit button.
- Ask the docs (AI, optional per space): a chat that answers from the space's published pages through
the instance's ai service, quoting and linking the pages it used; rate-limited per visitor; off unless the
space has an ai key.
- Feedback: "Was this page helpful?" with an optional comment, kept per page (a space keeps its newest
1000), rate-limited per visitor and per space.
- Analytics (optional per space): page views through the analytics service's site for the space, plus
docsd's own report of searches without results (the 1000 most frequent per space, recorded at a limited
rate per visitor and per space) and feedback. Count readers in settings makes that site in xvii
analytics with the admin's own session; the published site then loads its counter (not in previews). A
search a reader closes without results is a miss.
docs.<d> itself: the built-in xvii documentation (as in v1) and, for signed-in people, the panel.
Panel
At docs.<d> behind sign-in, built like every panel (app shell, team switcher). Inside a space the app
sidebar holds the space's sections and its page tree; the editor fills the main area like Notion's (title
and description in the page, / menu at the caret, a selection toolbar, block handles with their menu,
comments and history in a side sheet). Settings are sections with descriptions and one save bar that
appears with unsaved changes. Nothing on docs.<d> may be framed (X-Frame-Options: DENY,
frame-ancestors 'none'): user sites on sibling hosts are same-site with it.
- Spaces: a table with access, languages, version, last publication, views; create from a template or
import.
- Space: the editor (sidebar tree + page), publish bar with draft changes count, history, comments
panel, settings (general, access and sharing, guests, languages, versions, look, navigation, domains,
git source, AI, analytics), publications list, feedback, trash.
- Import: Markdown folder or zip (front matter and the v1 dialect understood), a Notion export (Markdown
& CSV zip), a GitBook export, a Docusaurus/MkDocs folder (front matter and sidebar files), an OpenAPI
document. Export: Markdown zip in the git layout, a single PDF of a version.
- AI in the editor (when the instance has ai): rewrite, shorten, fix grammar, translate into another
space language (into the translation of the page, as a suggestion), continue writing, summarize a page
into its description. Runs with a team key: Connect xvii AI makes a dedicated key of the team with the
signed-in admin's session (sealed, never sent to the browser, revoked on disconnect, limited to chat
models and 60 requests a minute) and picks a model; a team key can be entered by hand instead. AI
proposals are checked against the text they started from before they are applied.
API
GET|POST /api/v1/spaces · GET|PATCH|DELETE /spaces/{id}
GET|POST /api/v1/spaces/{id}/pages · GET|PATCH|DELETE /pages/{id} · POST /pages/{id}/move
GET /api/v1/pages/{id}/content?lang=&format=markdown|html · PUT (Markdown in, for scripts)
WS /api/v1/collab/{page}/{lang} Yjs sync + awareness
GET /api/v1/pages/{id}/history · POST /pages/{id}/restore
GET|POST /api/v1/pages/{id}/comments · PATCH /comments/{id} (resolve, edit) · DELETE
POST /api/v1/spaces/{id}/publish · GET /spaces/{id}/publications · POST /publications/{id}/live
POST /api/v1/spaces/{id}/versions · DELETE /versions/{id}
GET|POST /api/v1/spaces/{id}/shares · DELETE /shares/{id} page and space links
GET|POST /api/v1/spaces/{id}/guests · DELETE /guests/{id}
POST /api/v1/spaces/{id}/import · GET /spaces/{id}/export?format=zip|pdf&version=
POST /api/v1/spaces/{id}/assets (uploads) · GET /assets/{id}
POST /api/v1/git/{space}/hook repository webhook
POST /api/v1/spaces/{id}/ask AI answers from the publication
GET /api/v1/templates · POST /templates
Also: POST /pages/{id}/sync (the editor's state, saved before the answer), POST /spaces/{id}/git/sync
(sync now; 202 {"queued": true} while a sync runs), POST /spaces/{id}/git/unlock, POST|DELETE /spaces/{id}/ai/connection, GET /spaces/{id}/ai/models, POST /spaces/{id}/ai/assist, POST|DELETE /spaces/{id}/analytics/connection, GET|POST /hosts… for domains. Readers ask on the site's own host
(POST /api/v1/site/ask), rate-limited per visitor. PUT /pages/{id}/content takes text/markdown
(front matter title and description apply) or JSON. /pages/{id}/restore takes a page out of the trash,
or with {"snapshot": id} restores that snapshot.
Reading endpoints of a site (/api/v1/site/... on the space's host: nav, pages, search) follow v1's shape
and its access rules. MCP at /mcp for editors: list spaces and pages, read, create and update pages from
Markdown, publish (list_spaces, list_pages, read_page, create_page, update_page, publish,
with the caller's access, next to the built-in search_docs and read_doc; a request without a token
gets only those two); on a public site, search_docs and read_doc as in v1.
CLI
xvii docs spaces spaces of your teams
xvii docs create NAME [--template T] a new space
xvii docs publish [DIR] --space S import a Markdown folder and publish (CI-friendly, trust rules)
xvii docs pull --space S [DIR] export the draft as Markdown
xvii docs open [SPACE] the space's site or editor
xvii docs [QUERY…] search the built-in documentation (v1)
Data and limits
PostgreSQL: spaces, pages, Yjs update logs and snapshots, page snapshots (history), publications (Markdown
and HTML per page, compressed), versions, comments, shares, guests, feedback, templates, git sources. Assets
on disk under DATA_DIR/assets/<space>/ (sha256-addressed; an upload is checked against the limits before
it is stored there, and a file no asset refers to is removed after an hour). Limits: docs.spaces,
docs.pages, docs.storage (assets and publications), docs.guests. Events: space.published,
comment.mention, comment.created, git.sync_failed, share.created. The manifest gains database,
panel and wildcard.
Build order (v2.0.0 ships when all are done)
- Spaces, pages, access, the panel shell, the site engine serving publications by host; v1's built-in
space moved onto it.
- The editor: blocks ↔ Markdown, Tiptap + Yjs, the Go collaboration server, presence, history.
- Publishing, publications, versions, languages.
- Hosts and custom domains, sharing links and passwords, guests, look per space, analytics and feedback.
- Comments and mentions, templates, import and export, OpenAPI references, AI (ask and assist), git
source and
xvii docs publish.