Skip to content

Write the docs.plus documentation set, phase one: self-hosting and the API #170

Description

@HMarzban

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:

  1. Self-hosting. How somebody installs docs.plus on their own server and checks that it works.
  2. 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.

  • Two people who have never seen this repository follow docs/self-hosting/install.md on a clean machine and reach a working document at a URL, without asking a question. Record where each one stops.
  • An outside developer follows docs/api/quickstart.md only, and creates a document, writes content, and reads it back. Time it.
  • Every fenced command was run by hand, as written.
  • No environment variable is defined in two places under docs/.
  • README.md fits 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.md or any CLAUDE.md.
  • bun run check:agent-docs passes. Moving README and CONTRIBUTING text can break a tracked link.

Task list

  • Fix the /docs/ gitignore rule — resolved 2026-08-19 by abfd17b0a; 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.md and remove the setup steps CONTRIBUTING.md duplicates

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions