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.md first, 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, or npx. 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.md index. No llms.txt in 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>. Never YOUR_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.md keeps 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.json and GET /docs answer 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.txt file, or any second copy of a page written for machines.
- Markdown generated from OpenAPI.
- Any copy of
AGENTS.md or a CLAUDE.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.md carries 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.yml declares 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:643 sets external: true. The only creators are two GitHub workflows. make up-prod on a clean host fails before any container starts, so the reader needs docker network create docsplus-network as 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-shot migrate service. A plain up -d can 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:91 points the browser at /api/v1, and no such route exists. Line 90 uses /api correctly.
- 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_URL makes DOCX import drop images, and rate limiting disables itself when Redis is unavailable.
Done when
Every item is testable. Run them before closing.
Task list
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.
Goal
docs.plus has no documentation set. It has a long
README.md, aCONTRIBUTING.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:
Who reads it
In priority order. A reader nobody can name gets no page.
docs/self-hosting/install.mddocs/api/README.mdanddocs/api/quickstart.mdCONTRIBUTING.md. No new page.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 publicThis blocker is gone. Commit
abfd17b0aon 2026-08-19 deleted the/docs/rule from.gitignore, and commit85cb575cbadded the first pages the same day.git ls-files docs/lists 12 files today, andgit check-ignore -v docs/self-hosting/new-page.mdmatches 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 carelessgit add docs/leaks plans and reviews. Theabfd17b0acommit message and a.gitignorecomment block both record that reason. Private material lives inNotes/instead, which.gitignoreignores whole. Keepdocs/public, and put nothing private there.Structure
Index files are named
README.md, notindex.md, because GitHub renders aREADME.mdwhen a reader browses into a directory. That is what makes the reading path work with no tooling.Ownership, so no fact lives twice
docs/self-hosting/README.mdREADME.mdprerequisites sectiondocs/self-hosting/install.mdREADME.mdproduction deployment sectiondocs/self-hosting/configuration.mdREADME.mdenvironment files sectionapps/hocuspocus.server/ENV.mdstays the ownerdocs/api/API.mdsectionsapps/hocuspocus.server/API.mdstays the ownerAPI.mdis 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.mdgets shorterIt 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, sectionSimplified 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:
CONTEXT.mdfirst, then the code. Never paraphrase an identifier.npm,yarn,pnpm, ornpx. This repository is Bun only.Conventions that make the pages work for AI agents
Each of these is checkable, not aspirational.
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.###. No numbered headings, because an insert renumbers the rest. Rename a heading only in a change that updates every link to it.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.README.mdkeeps the emoji it already has; every reference document in this repository already uses none.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.
llms.txtfile, or any second copy of a page written for machines.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.install.mdcarries a verify section only.Truths the self-hosting page must state
Writing
install.mdhonestly 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.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.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.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.NEXT_PUBLIC_*values are build arguments. Editing them after a build changes nothing until a rebuild..env.example:91points the browser at/api/v1, and no such route exists. Line 90 uses/apicorrectly.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.
docs/self-hosting/install.mdon a clean machine and reach a working document at a URL, without asking a question. Record where each one stops.docs/api/quickstart.mdonly, and creates a document, writes content, and reads it back. Time it.docs/.README.mdfits on two screens, and every removed subject has one link to its new home.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 abovedocs/README.md— the hubdocs/self-hosting/README.md— requirementsdocs/self-hosting/install.md— clone to working site, plus verifydocs/self-hosting/configuration.md— env-file modeldocs/api/README.md— base URL, envelope, error codes, limitsdocs/api/authentication.md— the three credential typesdocs/api/quickstart.md— first working calldocs/api/websocket.md— connect a Yjs clientREADME.mdand remove the setup stepsCONTRIBUTING.mdduplicatesNotes 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.