Write the docs
How to add or fix a documentation page: branch, write MDX, run the content tests, add a changeset, and let the tagged deploy ship it.
Add or fix a page on these docs, from branch to the tagged deploy that ships it.
These docs live in the same repository as the product they describe, as MDX files in a folder tree. There is no CMS and no special access. If you can open a pull request on GitHub, you can fix a page, and a test suite pins the structure so a first-time contributor can add a file without studying a framework first.
What this site is
The site is apps/docs, a Fumadocs site on Next.js built to a fully static export and served at docs.subshell.sh. Three practical consequences:
- Pages are MDX files under
apps/docs/content/docs/. That is Markdown plus imports, but the build is static, so keep pages to prose, code blocks, tables, and GitHub-style alerts. A static build runs no interactive widgets. - The sidebar is a file tree, not a config database. Each folder has a
meta.jsonlisting its pages in order, and each page's sidebar label is its frontmattertitle. - Search is a static index built at compile time, and the site emits
llms.txtandllms-full.txtfrom the same frontmatter. Write thedescriptioncarefully: it is what search results and agents quoting these docs will read.
The content tree
apps/docs/content/docs/
├── meta.json # sidebar group order
├── index.mdx # landing page
├── get-started/
│ ├── meta.json # { "title": "Get Started", "pages": [ ... ] }
│ ├── index.mdx
│ └── …
├── use/
├── agents/
├── nodes/
├── server/
│ └── network-plugins/ # folders nest, each with its own meta.json
├── automation/
├── concepts/
├── reference/
├── help/
└── develop/A meta.json pages array holds bare filenames (without .mdx) or subfolder names, in sidebar order. A page not listed anywhere is an orphan, and the test suite fails on one deliberately: an unlisted page is a page no one can navigate to. The root meta.json order is pinned additionally by the ROOT_PAGES list in the content test, so changing the top-level groups means editing that test too. Not every folder carries an index page: automation, reference, and develop have none.
Frontmatter
Every page starts with this schema:
---
title: Sharing
description: Who can see and interact with a subshell, and how grants work.
icon: Share2 # optional: a lucide icon name for the sidebar
---title and description are required, and a page missing either fails the build. Write the description as one sentence that would read well alone in a search result.
Add or change a page
-
Fork the repository and branch from
main. One branch per topic keeps review simple. -
Write the page. Drop a
.mdxfile in the group that owns the topic, with the frontmatter above. If it is a new page, also add its filename to that folder'smeta.jsonpagesarray, in the position it should appear. -
Install and run the site:
bun install bun run dev:docs # serves the site at http://localhost:3400The dev server is separate on purpose: it is not part of the product's
bun run start, so working on the app never opens a docs window on anyone. -
Run the content tests:
cd apps/docs && bun testThe suite enforces the contract: every
meta.jsonentry resolves to a page or folder, every page hastitleanddescriptionfrontmatter, no.mdxfile is orphaned, every root-relative internal link points at an existing page, and no page carries an em dash. Green here plus a rendering check in the dev server covers everything CI will check about your prose structure. -
Add a changeset if your change should ship soon. A docs change goes live on a tagged deploy (described below), and tags come from version bumps, which come from changesets:
bunx changeset # select @internal/docs, describe the changeA drive-by typo fix can skip this: it merges freely and ships with the next version PR. Anything you would like users to see within days should carry one.
-
Open a pull request. First-time contributors are asked once to sign the CLA: you keep copyright, and the grant is what keeps the project's dual licensing possible. A maintainer reviews, and every page of these docs carries an Edit-on-GitHub link for exactly this loop.
How your change reaches the site
Merging to main does not deploy. The process mirrors the product's releases: pushes to main keep a "chore: release package(s)" version PR current, and merging it bumps @internal/docs and its CHANGELOG. Then a maintainer dispatches the docs workflow, which pushes the docs-vX.Y.Z tag and deploys:
gh workflow run docs.ymlThe workflow is dispatch-only, and it has no CI gate: every push builds the static export, and PR CI checks it before that, so the export is proven before the deploy exists. Tagging and releasing is owned by the workflow, never a hand-made tag. Your PR merging is step one, the version PR merging is step two, and the dispatch is step three. If you are not a maintainer, the second and third steps are maintainer actions; a reviewer will tell you when your change is queued.
Finishing a stub page
Many pages here are honest stubs: a real intent paragraph, a Draft alert, and a "Sources" section naming the repository files the content will be migrated from. Completing one is the workflow above with an editorial contract:
- Read every source in the stub first. They are all real paths in this repository. Where two sources disagree, the app's documentation for that area wins, and the disagreement is worth reporting.
- Write for the reader who may be on any device (desk terminal, laptop, tablet, phone), not for a contributor reading the architecture. Cover the intent the stub paragraph promises.
- Remove the Draft alert. That is the lifecycle event that makes the page real.
- Convert the Sources section into a proper references or "see also" list.
docs/security.mdstays the authority for security claims; pages here summarize it and link it rather than restating its tables. - Answer open questions, do not guess. Where the stub left a question open, get an answer from a maintainer rather than guessing product behavior. Never invent a flag, a default, or a permission.
Voice and vocabulary
Prose is governed by the docs style guide, apps/docs/STYLE.md: sentence rhythm, plain task-shaped headings, neutral warnings, and the page shape. The technical contract (vocabulary, MDX rules, never invent) lives in apps/docs/AGENTS.md. Read both before writing; the models are the Tailscale and NetBird documentation.
-
Every page begins with exactly one plain sentence that says what the page does. Name the task and stop there. No rollup of outcomes, no alternatives or links, no platform or requirement detail in that sentence: requirements go under "Before you start", and the other routes to the same destination get named once at the bottom.
-
Second person, user-facing, plain. "You grant view access…", not "the system permits…". Commands you expect the reader to run go in fenced blocks with no prompts.
-
No em dashes. Use a comma, a colon, parentheses, or a full stop and a new sentence instead; the em dash tends to mark a sentence that kept growing. The rule covers prose, headings, frontmatter, and callouts, and the content test fails any page carrying U+2014, so fix it before you push. En dashes in ranges and hyphens are not covered; leave them alone.
-
The three words are load-bearing, and these docs are where users learn them:
word means is not server the control plane: API, database, the SPA it serves a machine that runs agents node a machine that runs agents (the subshelldaemon)a user-facing app client a human interface to a control plane: web, mobile, desktop the node daemon -
Display labels follow the product: "Subshell Server" and "Subshell Client" are the desktop apps;
subshell-serverandsubshell(in code font) are the CLI binaries; the control-plane host's node row is named by an admin and defaults to "Server". Harnesses are plugins. Reusing a word for two things is the one style violation that will bounce a PR.
MDX notes
-
Callouts use GitHub alert syntax and render as styled boxes:
> [!note] Why the default is private > A subshell is invisible to other users until you share it.note,tip,important,warning, andcautionall render;warningis what stubs use for their Draft banner. -
Curly braces are code in MDX. A literal
{in prose starts a JSX expression and breaks the build, so wrap such text in backticks.`--from <file>`is fine; a bare<file>in prose is not. Angle brackets likewise start JSX, so keep flags, placeholders, and paths in code font. -
Links between pages are root-relative:
[Nodes](/nodes),[the threat model](/concepts/security-model). The content test resolves every internal link, so a dead path fails the suite the same as broken prose. -
Code fences carry languages (
bash,json,ts). Screenshots do not exist yet; describe in words what a screen shows until the screenshot pass lands. -
When CLI behavior changes, the docs change with it. The reference pages transcribe the usage blocks in
apps/server/api/src/cli.tsandapps/node/agent/src/cli.ts, so a PR that edits either block should update the matching reference page in the same PR: one repo, one review.
Where to ask
Open an issue on the repository for anything structural: a missing group, a page that should exist and does not, or a stub whose sources are wrong. Content questions get answered fastest in the PR itself.
Other pages
- Contribute to Subshell: the product's dev environment, which hosts this site
Last updated on
