Repository navigation
Write the docs.plus documentation set, phase one: self-hosting and the API #170
Description
Goal
docs.plus has no documentation set. It has a long README.md, a CONTRIBUTING.md, and a 960-line API contract inside a package directory. A person who wants to run docs.plus on their own server, or call its API from their own code, has no page written for them.
Phase one writes that set as organised Markdown in this repository. No documentation website, and no static-site generator.
Two subjects come first:
- Self-hosting. How somebody installs docs.plus on their own server and checks that it works.
- The API. How an outside developer authenticates and makes a call.
Who reads it
In priority order. A reader nobody can name gets no page.
| Reader | Arrives wanting | Page that must exist |
|---|---|---|
| Self-hoster | A requirement list, and a numbered path from clone to a working site | docs/self-hosting/install.md |
| API consumer | The base URL, the auth header, and one call that works | docs/api/README.md and docs/api/quickstart.md |
| Contributor | A local stack that starts | Already served by CONTRIBUTING.md. No new page. |
| Extension user | How to install one of the five npm packages | Already served by extensions/README.md. No new page. |
An AI agent is not a separate reader. People point coding agents at documentation, so the pages must work as agent input. That is a set of writing rules, not a second copy of the pages. A separate agent-facing mirror would double every fact and one copy would go stale.
Resolved: docs/ is tracked and public
This blocker is gone. Commit abfd17b0a on 2026-08-19 deleted the /docs/ rule from .gitignore, and commit 85cb575cb added the first pages the same day. git ls-files docs/ lists 12 files today, and git check-ignore -v docs/self-hosting/new-page.md matches no rule, so a new page commits cleanly.
The ignore-then-negate shape this issue first proposed was rejected on 2026-08-19. That shape was /docs/* with one ! line per public path. It mixes public pages into a private directory, so one careless git add docs/ leaks plans and reviews. The abfd17b0a commit message and a .gitignore comment block both record that reason. Private material lives in Notes/ instead, which .gitignore ignores whole. Keep docs/ public, and put nothing private there.
Structure
docs/
├── README.md # the hub: one table, every page
├── self-hosting/
│ ├── README.md # requirements, and which path to take
│ ├── install.md # clone to a working site, then verify
│ └── configuration.md # which file you edit, and which process reads it
└── api/
├── README.md # base URL, response envelope, error codes, rate limits
├── authentication.md # the three credential types, and which surfaces accept each
├── quickstart.md # create a document, write content, read it back
└── websocket.md # connect a Yjs client to a document room
Index files are named README.md, not index.md, because GitHub renders a README.md when a reader browses into a directory. That is what makes the reading path work with no tooling.
Ownership, so no fact lives twice
| Fact group | Owner after phase one | Copy that dies |
|---|---|---|
| Requirements and version floors | docs/self-hosting/README.md |
README.md prerequisites section |
| Install and verify | docs/self-hosting/install.md |
README.md production deployment section |
| Which env file, and which process reads it | docs/self-hosting/configuration.md |
README.md environment files section |
| Backend variable names, types, defaults | apps/hocuspocus.server/ENV.md stays the owner |
none |
| Auth, response envelope, error codes, rate limits | docs/api/ |
the matching API.md sections |
| Route-by-route contract | apps/hocuspocus.server/API.md stays the owner |
none |
API.md is not copied into Markdown page by page. It stays the full engineering contract. The new API pages own the parts an outside developer needs first, and link to it for the rest.
README.md gets shorter
It carries eleven sections today and does the job of a whole documentation set. Target is a hero image, one paragraph, a three-command start, a documentation table, the extensions table, contributing, license, and support. Everything else moves to docs/ and leaves one link behind.
Also cut the pinned version numbers from the front page. They rot on every dependency bump and nobody updates the README for it.
Writing standard
The house standard is .cursor/skills/tech-writer/SKILL.md, section Simplified English (house standard). Read it before writing. It is mandatory, not advice. English is not every reader's first language, so write at CEFR B2 or C1. Simplify the English. Never simplify the engineering.
The rules that bite most often here:
- One sentence, one fact, 25 words maximum. Verbatim material does not count toward the cap.
- Lead with the answer. The line under the H1 says what the page covers and what it does not, and links the page that owns the rest.
- Verb first, active voice, present tense.
- One name per thing across the whole set, not only inside one page. Resolve names from
CONTEXT.mdfirst, then the code. Never paraphrase an identifier. - End every warning and limit with the next step.
- Show the expected result. A command gets its output. A request gets a response body.
- Never write
npm,yarn,pnpm, ornpx. This repository is Bun only. - No marketing words, no hedges, no idioms, no "see above".
Conventions that make the pages work for AI agents
Each of these is checkable, not aspirational.
- One
docs/README.mdindex. Nollms.txtin phase one. That convention is a file served over HTTP, and phase one serves nothing, so a hand-written second index would drift the first time a page moves. - A summary line under every H1. One or two sentences naming scope and exclusions. An agent often retrieves one page with no surrounding context, so this line is the only context it gets.
- Stable anchors. One H1 per page. No heading deeper than
###. No numbered headings, because an insert renumbers the rest. Rename a heading only in a change that updates every link to it. - No YAML front matter. Nothing in phase one parses it, and it puts the title in two places.
- Every fenced block carries a language tag, and one block has one purpose. Never mix a command and its output in one block, because an agent copies the whole block.
- Placeholders use angle brackets. An environment variable keeps its exact name, as in
Authorization: Bearer <SUPABASE_SERVICE_ROLE_KEY>. Anything else is lowercase, as in<documentId>. NeverYOUR_KEY_HERE, never a value that looks real, because an agent copies a real-looking value and ships it. Every placeholder gets one line under the block saying where the value comes from. - No emoji in new pages. An emoji in a heading changes the GitHub anchor. The root
README.mdkeeps the emoji it already has; every reference document in this repository already uses none. - The generated OpenAPI document is not the source of truth for behaviour. It owns request and response schemas, and it cannot drift, because it is built from the live zod schemas. Prose owns authentication, ordering, durability, and failure handling. Say once, on each API page, that
GET /openapi.jsonandGET /docsanswer in a local or internal environment only.
What phase one does not include
Each of these grows on its own unless it is named here.
- A documentation website, a generator, hosting, or a search index.
- An
llms.txtfile, or any second copy of a page written for machines. - Markdown generated from OpenAPI.
- Any copy of
AGENTS.mdor aCLAUDE.md. Those are rules for coding agents changing this repository. They are not reader documentation, and a reader takes a prohibition as a product statement. - An operations runbook. Backup, restore, upgrade, and scaling wait for a later phase.
install.mdcarries a verify section only. - An architecture deep dive, a concepts book, or a tutorial series.
- Per-extension usage guides. The five package READMEs already ship on npm.
- Translations, versioned documentation, or client SDKs.
- New tests, a prose linter, or a link-check service.
- More than two screenshots. They go stale and nobody updates them.
Truths the self-hosting page must state
Writing install.md honestly means stating these. Each is verified in this tree today. Several are defects rather than documentation gaps, so file a separate issue where a fix is the better answer.
- There is no database in the production compose file.
docker-compose.prod.ymldeclares Traefik, Redis, a migration job, and the five application services. PostgreSQL and Supabase are external dependencies the reader must provide. The README currently lists a managed database as a recommendation. - The Docker network is external and nothing creates it outside CI.
docker-compose.prod.yml:643setsexternal: true. The only creators are two GitHub workflows.make up-prodon a clean host fails before any container starts, so the reader needsdocker network create docsplus-networkas an explicit step. - Traefik hostnames are hard-coded to the maintainer's domains in five label lines. No environment variable covers them, so a stranger's deployment routes nothing until those lines are edited.
- The migration job has no ordering guarantee. All three backend services set
RUN_MIGRATIONS: '0', and nothing depends on the one-shotmigrateservice. A plainup -dcan start the applications against an unmigrated database. The reader must run the migration as its own step first. - Supabase SQL has no deploy pipeline. The reader applies the numbered scripts by hand, and the correct order is not obvious from the directory. Publish the ordered list.
NEXT_PUBLIC_*values are build arguments. Editing them after a build changes nothing until a rebuild.- Local disk storage does not work in production. Two REST replicas run with no shared volume, so uploads land in one container and vanish on redeploy.
.env.example:91points the browser at/api/v1, and no such route exists. Line 90 uses/apicorrectly.- The worker is not optional. It owns document persistence, email, and push. Without it, saves strand in Redis and their payloads expire after one hour.
- Redis is not a cache here. It carries cross-replica sync, the queues, and the persistence claim-check payloads. Eviction is a data-loss path.
- Never route port 4003. It carries service-role write endpoints, and only network isolation protects it.
- Things that silently do nothing when misconfigured: an unset
PUBLIC_RESTAPI_URLmakes DOCX import drop images, and rate limiting disables itself when Redis is unavailable.
Done when
Every item is testable. Run them before closing.
- The three outside-reader checks moved to Verify the self-hosting and API docs with outside readers #533.
- No environment variable is defined in two places under
docs/. -
README.mdfits on two screens, and every removed subject has one link to its new home. - An agent answers three questions from this repository alone, correctly: how do I authenticate, which header carries the user token, and which port does the REST API listen on.
- No page repeats a fact from
AGENTS.mdor anyCLAUDE.md. -
bun run check:agent-docspasses. Moving README and CONTRIBUTING text can break a tracked link.
Task list
-
Fix the— resolved 2026-08-19 by/docs/gitignore ruleabfd17b0a; see Resolved above -
docs/README.md— the hub -
docs/self-hosting/README.md— requirements -
docs/self-hosting/install.md— clone to working site, plus verify -
docs/self-hosting/configuration.md— env-file model -
docs/api/README.md— base URL, envelope, error codes, limits -
docs/api/authentication.md— the three credential types -
docs/api/quickstart.md— first working call -
docs/api/websocket.md— connect a Yjs client - Slim
README.mdand remove the setup stepsCONTRIBUTING.mdduplicates
Notes for whoever picks this up
The API pages need the deepest product knowledge, and the self-hosting pages are best written by somebody who has actually self-hosted docs.plus. Those two halves are independent once the hub exists, so they can run in parallel.
Say so in a comment if you want this split into separate issues per page group. A single issue nobody can finish is worse than three that people can claim.
Activity
docs: add reader documentation for self-hosting and the API
HMarzban commentedon Oct 19, 2026
The three outside-reader checks (two clean-machine self-hosting runs, one API quickstart run, and running every fenced command by hand) moved to #533, so this issue can close on its writing work alone.
HMarzban commentedon Oct 19, 2026
Checked 2026-10-19, with maintainer rulings
docs/: fixed (bebf75c6d).RATE_LIMIT_MAXnow lives indocs/api/README.md#rate-limiting.PERSIST_TO_LOCAL_STORAGE,STORAGE_TYPEandNEXT_PUBLIC_VIRTUOSO_LICENSEnow live indocs/self-hosting/configuration.md.README.mdon two screens: moved to v2.3.0 (ruling 2026-10-19), as Shrink README.md to two screens and move its detail into CONTRIBUTING.md and docs/ #541, with a section-by-section plan.docs/api/authentication.md.Authorization: Bearerortoken:docs/api/authentication.md.docs/api/README.md.auth.ts,env.schema.ts).AGENTS.mdorCLAUDE.mdfact: pass, on the loose reading (ruling 2026-10-19). No docs page copies an agent rule. Where a fact appears in both, the docs own it, and the agent file holds it only as context for a rule. One page did copy a rule, the Bun-only sentence indocs/self-hosting/README.md, and it is removed.The human-run checks moved to #533 earlier. Closing.
docs(self-hosting): give each environment variable one home