Documentation
README
Carbon Docs
docs is a built, opinionated documentation site β Fumadocs + Next.js (React 19), light-only,
warm-paper aesthetic. It is no longer scaffolded; it exists and ships content. This skill is how to add
or change docs in that system, in its house style, grounded in real Carbon code.
Announce at start: "Using the carbon-docs skill β authoring docs for {topic}."
The single biggest mistake is writing plausible ERP-generic prose. Carbon's behavior is specific and often counterintuitive (WIP is a GL balance not a table; a payment is a posted entity applied via settlements; overhead IS absorbed into WIP per production event; fixed-asset disposal is scrapping-only). Every claim is grounded in source. See the prime directive below β it overrides everything.
What Carbon is: a manufacturing system β ERP (the office) + MES (the floor), one platform over one data model. Academy is not a third product pillar: it just hosts training video lessons, not a SaaS app a customer buys. Never frame Carbon as "ERP, MES, and training/Academy"; site, meta, and landing copy stay ERP + MES. (The
academyapp can appear in an architecture/monorepo listing β fine; describe it as video-lesson hosting, not a pillar.)
The prime directive β ground everything in source
The holy source of truth is the actual source code + the LATEST database migrations
(packages/database/supabase/migrations/, newest by timestamp) β NOT ERP/CMMS general knowledge, NOT
.claude/rules/ alone (it is often stale).
- Verify before you write. Every entity, status enum value, and transition named in docs must exist
in real code. Confirm exact strings (
"To Ship and Invoice","Fully Depreciated"), the actions that drive them (service fns, routes, edge functions inpackages/database/supabase/functions/), and what posts/gates (e.g.companySettings.accountingEnabled). - Read the newest migration, not the first. Timestamps order them; a 2026 refactor may have rebuilt a subsystem the cache still describes the old way.
- Document only real, ACTIVE features. Omit placeholders / inactive / not-yet-shipped things (e.g.
integration registry entries with
active: falselike QuickBooks/Sage/Zapier). Don't surface them. - When code and cache disagree, code wins β and note the drift.
- Method: dispatch a research subagent per feature β return verified facts with
file:linerefs β then write. This is how every flow in the Guide was built. Don't skip it for anything non-trivial.
Three surfaces (know which you're touching)
| Surface | Path / route | What it is | Authored? |
|---|---|---|---|
| Guides | content/guides/*.mdx β /guides |
Editorial narrative tours, grouped into flows. Second-person, opinionated, illustrated. The journey + the "why". | Hand-written MDX |
| Reference | content/docs/**/*.mdx β /docs |
One page per entity/concept. Scannable: tables, cards, field rows. The "what". | Hand-written MDX |
| API reference | app/api-reference/[module]/[resource] |
PostgREST endpoint docs, generated at build from the swagger. | Generated β do NOT hand-edit. Edit scripts/generate-api-docs.mjs or the swagger schema. |
Interlink across surfaces and flows: a Guide links into Reference for detail; Reference links back to the Guide for the story. (Guide chapters were given 12 cross-flow links β interlinking is expected, not optional.)
Authoring workflow (every change)
- Research (grounded). Subagent verifies the feature vs source + newest migrations. Get exact
table/column/enum/transition names +
file:line. Flag what a generic description would get wrong. - Pick the surface + placement. Guide flow + frontmatter, or Reference folder +
meta.jsonorder. - Write in the house voice with the surface's real components (below). Lead with a concrete example.
- Verify β against the user's running dev server, read-only. They usually have
pnpm --filter docs devup. Neverpkill/restart it, runnext build, orrm .nextunder it β verify by fetching pages:
A greencd docs pnpm exec fumadocs-mdx # regen .source if a content/frontmatter change didn't HMR curl -sS -X GET http://localhost:3002/docs/<slug> | grep -o "your new heading" # confirm it renderedpnpm --filter docs build(β Generating static pages N/N) is the gold standard β but only in a clean checkout / when no dev server is running.tscis noisy (a React 18/19@typesskew thatnext.configignores) β grep your own files out of the errors rather than expecting zero.
Guides β the flow architecture
Frontmatter (source.config.ts schema):
---
title: From quote to order
description: An RFQ becomes a quote; an accepted quote becomes a sales order.
label: "(I)" # display marker, roman numeral, per-flow
index: 0 # order WITHIN the flow (0,1,2β¦)
flow: quote-to-cash # flow id (omit β defaults to "make-to-order")
flowName: Quote to cash # flow tab label
flowIndex: 1 # order of the flow in the subnav (0 = first)
---
- Chapters sort by
(flowIndex, index). The subnav is a flow switcher; the **sidebar + mobile selector- "read next" are scoped to the active flow**. The original 5 chapters (order/build/plan/floor/ship) carry
no flow fields and fold into
make-to-order(flowIndex 0) via defaults.
- "read next" are scoped to the active flow**. The original 5 chapters (order/build/plan/floor/ship) carry
no flow fields and fold into
- Add a chapter to a flow: same
flow/flowName/flowIndex, nextindex+label. - Add a flow: new
flowIndex+flowName; its chapters start atindex: 0,label: "(I)". - All chapter bodies render server-side on every page (the reader cross-fades, no route nav), so links to
/guides/<slug>resolve. No code fences in guides (prose + components only).
Components (components/editorial/mdx.tsx):
<Figure illustration="flow-overview" caption="β¦" />β SVG from the registry.illustrationMUST be a real key fromcomponents/editorial/illustrations.tsx(else it silently renders nothing). Valid keys:flow-overview, order-split, bom-tree, demand-forecast, planning-engine, shopfloor-loop, eight-d, traceability-graph, method-types, kit-vs-subassembly, reorder-policy, outside-processing, mes-station, issue-workflow, schedule-board, get-method, conversion-factor, opportunity-thread, cash-cycle, rfq-fanout, receive-bill-axes, wip-inflow, wip-to-cogs, depreciation-curve, asset-exit. For anything without a fitting key, use<Screenshot>instead β don't invent keys.<Screenshot label="Sales order dashboard" caption="β¦" ratio="wide|tall|square" />β a standing slot for a real Carbon capture. Use it where the reader needs to see the actual UI (dashboard, a specific form/field, status, board, where-to-click) β not as decoration. Label the real, current screen + state precisely (verify it exists) so a real screenshot can drop straight in and the docs stay synced to the live Carbon UI. Defaultratio="wide".<Callout tone="neutral|blue|green|amber" badge="WHY BATCH" title="β¦">body</Callout>β the workhorse. Use it to carry the Carbon-specific truth a generic description gets wrong. Tones: blue = explanatory "why", green = good-to-know/outcome, amber = caution/contrast, neutral = definitional.<Divider />β closes a chapter before its wrap-up line.<Term>make to order</Term>β inline glossary term: dotted-underline; click/tap opens a popover with a grounded one-line definition + an optional "Learn more" link. Same component on both surfaces; definitions live in@carbon/glossary(packages/glossary/src/terms.ts), rendered bydocs/components/editorial/glossary.tsx. See "Interlinking & the glossary" below.
Each ## heading becomes a sidebar rail entry β so structure chapters as 3β5 ## sections.
Reference β entity pages
- Frontmatter:
title+descriptiononly. - Nav =
meta.jsonpagesarrays (ordered). Folders:content/docs/{reference,platform,integrate}/. Root order incontent/docs/meta.json; a folder's order + sidebar title in its ownmeta.json({ "title": "Product reference", "defaultOpen": true, "pages": [...] }). Add a page β add its slug to the folder'spages. Don't listindexinpagesβ fumadocs treatsindex.mdxas the folder index and the nav renders it as "Overview"; listing it duplicates the title as a sibling. Integrations are their own top-level section (content/docs/integrations/) grouped by category β document onlyactiveintegrations (omitactive: falseplaceholders + commented-out ones). - Components (
components/editorial/reference-components.tsx+components/mdx.tsx):<Callout type="info|note|warn|warning|error|success|tip" title?>β¦</Callout>β type β badge+tone (info/noteβNOTE/blue,warn/warning/errorβHEADS UP/amber,success/tipβGOOD TO KNOW/green). Note this is a different Callout API than the Guides one (type vs tone+badge).<Cards><Card title href icon?>β¦</Card></Cards>β link-card grid (cross-surface navigation).<EnvVars><EnvVar name type? default? required?>β¦</EnvVar></EnvVars>β field/parameter rows. The env-var page uses these; for an entity's field reference prefer<FieldTable>(below).<FieldTable><Field name type? required?>desc</Field></FieldTable>(components/editorial/field-table.tsx) β the accordion field/parameter reference, the warm-paper take on Fumadocs' TypeTable. Use it for a| Field | Type | Description |-shaped table (item/job/line fields, routing, work-center, reorder/shelf-life policy fields, integration settings).type= the type token (omit when the table has no type column);requiredonly when the source marks it so; the child is the description as MDX (so inline`code`, italics,<Term>render β that's why it's children, not atype={{}}prop). Registered in bothmdx.tsxandeditorial/mdx.tsx.<StatusFlow><Status name accent? branch? terminal?>meaning</Status></StatusFlow>(components/editorial/status-flow.tsx) β an interactive lifecycle widget (selectable pills β a detailCalloutshowing the meaning) that replaces a linear| Status | Meaning |table. Children in source (lifecycle) order; meanings are MDX. Flags:accent= the single pivotal milestone (β€1, optional);branch= a temporary returnable hold (Paused, On Hold, Needs Approval);terminal= an off-path exit (Cancelled, Voided, Lost, Expired). Use it ONLY for a linear lifecycle β a 2-axis comparison matrix (e.g. invoices sales-vs-purchase) or a small enum list stays a markdown table.<PlanBadge plan="Business" />β flags a paid feature. Whole-page gate β setplan: Businessin frontmatter; renders a small "Paid" pill inline with the page title (label is fixed to "Paid"; theplanvalue only feeds the hover tooltip + the banner copy). Section gate β drop<PlanBadge>in-body. Gated features =packages/ee/src/plan.ts. Don't badge free ones (email, exchange-rates).- Plan-gated pages also get a full-width announcement bar (
components/plan-banner-bar.tsx,PlanBannerBar) rendered by the docs layout (app/docs/[[...slug]]/layoutbuilds a urlβplan map fromplanfrontmatter and passes it in) β sticky under the header, spanning the content area. Addingplan: Businessfrontmatter is all it takes; the badge and the bar both light up. Don't hand-place a banner in MDX. - Licensing model β get this right, it's been wrong before. See
docs/content/docs/platform/licensing.mdx(the canonical page). Editions (packages/utils/src/types.tsenum Edition): Community (self-host, AGPLv3 open core), Enterprise (self-host + commercial license), Cloud (managed). EE features = code underpackages/eeor any.eefile β require a commercial license per the repoLICENSE.- Carbon Cloud = the hosted SaaS at https://app.carbon.ms β NOT the
/docs/platform/...deployment docs. Self-hosting recipes (Docker with Caddy, AWS with SST) live underdocs/content/docs/platform/self-hosting/. - Cloud plans = Starter and Business only.
Planenum also hasPartner, but Partner is internal-only β never mention it in reader-facing docs. Don't write "Business and Partner plans". - Never write "self-hosted isn't plan-gated." The runtime gate is Cloud-only
(
packages/ee/src/plan.server.tsreturns early whenCarbonEdition !== Edition.Cloud), but the license still governs: self-hosting runs in community mode; Enterprise/EE features need a commercial license. Frame it as a license boundary, not a technical lockout β features aren't "disabled" in Community.
- Carbon Cloud = the hosted SaaS at https://app.carbon.ms β NOT the
<Steps>/<Step>,<Tabs>/<Tab>(fumadocs-ui), markdown tables, and code fences (dark panel).<Term id?>β¦</Term>β inline glossary term (dotted underline β definition popover). The same component as the Guide; see "Interlinking & the glossary" below.
- Voice is more technical/scannable than the Guides β fields, constraints, tables β but still names the gotcha and links back to the Guide for the narrative.
House voice
- Second person, concrete, narrative. Anchor in the running example (the 90-unit humanoid-robot order). "Open the sales order dashboard." / "You don't build 90 robots as one monolithic job."
- Quote real status names exactly, in quotes:
**"To Ship and Invoice"**,**"Posted"**,**"Open"**. Status strings live on a specific entity β confirm whether a value belongs to the header or the line (e.g."To Ship and Invoice"/"To Invoice"aresalesOrderStatus, NOT thesalesOrderLineStatusenumOrdered/In Progress/Completed) before you attribute it, and state the transition fully (an order at"To Ship and Invoice"flips to"To Invoice"once everything has shipped but isn't billed β don't imply it sits at one value until fully closed). - Go easy on em-dashes. Stacked, they read as a tic β at most one per paragraph, and never a dash-pair
parenthetical in an opening sentence. Default to a period or comma; reach for the dash only when it truly
beats both. ("Too many em-dashes" is the single most common copy note from review.) Applies to
caption=andtitle=strings too, not just body prose. - Keep the running example's nouns and numbers exact. It's the sales order (don't drift to a bare "order" when instructing the reader) and it's 90 units (not "a robot"). Once you name the entity and the quantity, stay consistent every time β drift is what makes a tour feel sloppy.
- Ground every "where to click." When you tell the reader to act in the UI, copy the real button / modal / field labels from the JSX (in quotes) and confirm the capability exists on that exact path. A feature that lives elsewhere is not the same as one on the screen you're describing β "Carbon splits the order into three jobs" was wrong: the sales-order line's "Make to Order" β "Convert Line to Job" dialog makes one job per click (the N-jobs-of-M split is a separate bulk-jobs flow). Verify the action, not just the concept.
- Callouts carry the counterintuitive truth β the thing people get wrong. Title is a claim, body is the why. ("Quotes are optional β the opportunity is the thread.")
- Answer the reader's cross-question where it arises. Every claim raises a next question in the reader's
head ("what if the job already started?", "does this touch existing orders?", "can I undo it?"). Anticipate
the one or two most likely at each step and answer them inline, at that point in the prose β a short
sentence, a Callout, or a
<Term>gloss β never deferred to a separate FAQ section or page. Ground the answer in source like any other claim; if you can't answer it from code, that's a research gap, not a skip. A section that leaves its obvious "but what aboutβ¦" hanging isn't done. - Explain the why, name the mistake, point to the next step. If a paragraph does none of those, cut it.
- Interlink at natural seams (
[make-to-order tour](/guides/order)). - Lead, don't label. Don't open a page with a generic, repeated heading β no
## Introductionon guide chapters (the chapter title is rendered for you) and no## Why it matterson reference pages. Open with 1β2 substantive lead sentences, then go straight to the real sections. A heading that's identical on every page is filler. Landing/index pages are "Overview", never "Introduction". - Name things as they are now. Use a feature's current name; never narrate rename/removal history ("formerly item rules", "storage units used toβ¦"). When an old name and the code disagree, the code wins.
Interlinking & the glossary
Two linking mechanisms β use both, deliberately, every time you touch a page. Internal linking compounds: a page that links out and glosses its jargon is worth more than the same prose in isolation.
- Markdown links carry navigation. Link the noun, inline, at natural seams; never "click here". A Guide
links into Reference for fields; Reference links back to the Guide for the story
(
[make-to-order tour](/guides/order)). Cross-surface and cross-flow links are expected, not optional. But link only when the destination's title echoes your anchor text. A hard jump whose landing page has a different H1 disorients the reader β linking the phrase "quote to cash" to a chapter titled From quote to order got flagged as confusing. When the anchor is a concept (a flow name, a category) rather than a page literally called that, gloss it as a<Term>(definition popover + optional "Learn more" viahref) instead of a bare link β the reader gets the meaning in place and can still navigate if they choose. <Term>carries definitions. Wrap a manufacturing/Carbon term a reader can hit cold (method type, replenishment system, WIP, outside operation, kit/subassemblyβ¦) so a click gives the gloss without leaving the page.<Term>make to order</Term>slugifies the text to find the entry;<Term id="make-to-order">made</Term>when the display text differs from the slug.- First occurrence per page only β not every instance. Underlining every "order" is noise.
- Definitions are a single source of truth:
@carbon/glossary(packages/glossary/src/terms.ts), where each entry isslug β { term, definition }withterm/definitionas Linguimsgdescriptors (rendered bydocs/components/editorial/glossary.tsx). Add the entry there before you use a new term, and ground the definition in source (the prime directive applies β exact enum values, real behavior). Omithrefwhen there's no dedicated page yet (the popover still shows the definition); the "Learn more" link auto-hides when it would point at the page you're already on. - Unknown slug β renders as plain text (never breaks prose) β a typo fails safe, not loud.
Enrichment pass. Whenever you create or edit a page, finish with a linking pass: wrap first-occurrence
jargon in <Term>, add markdown cross-links at the seams, and add any missing glossary entries. Cheapest way
to raise the whole site's connectivity.
Design / styling
- Light-only. Warm-paper palette: page bg
#FBFBF9/#F5F5F2; ink#262323(+rgba(38,35,35,0.x)for faint); accent#1E84B0(links) /#00B0FF(focus/brand); hairlines#E7E7E3/#E3E3DF. - Inline Tailwind arbitrary values (
text-[15px],bg-[#FBFBF9],border-[#E7E7E3]) β this app is standalone (NOT@carbon/react, NOT the ERP theme). Match the surrounding component's density and colors; don't introduce a new palette. - Fonts: DM Sans (body), Fira Code (mono). C