Skip to content

Docs Framework — Agent-Native Docs-as-Code Stack

Date: 2026-04-21 Status: Locked (design approved 2026-04-21) Owner: acidrums7 Scope: Ideony repo; Jooice federation considered but out-of-scope for initial rollout.

What this spec is: the WHAT — framework pick, repo layout, auto-gen pipelines, quality gates, cofounder access, CI/hosting. What this spec is NOT: the WHY (will spawn ADR once locked) and the HOW-TO (will live in docs/testing.md-style doc under a new docs/docs-framework.md after implementation).


  1. Locked goals
  2. Framework pick
  3. Repo layout
  4. Auto-gen pipelines
  5. Quality gates
  6. Zero-drift gate audit
  7. Agent-native layer — llms.txt + search
  8. Diagram-as-code standard
  9. Cofounder / dual-audience access
  10. Versioning strategy
  11. Federation hook — Jooice later
  12. CI + hosting pipeline
  13. Migration + rollout phases
  14. Risks + mitigations
  15. Out of scope

Ten goals locked during brainstorm (2026-04-21).

#GoalSource
1Total coverage — every shipped surface documenteduser
2SSoT / zero drift — code change without matching doc fails CIuser + existing check-docs-updated.sh
3Agent-optimized search (primary audience = Claude)user “e”
4Auto-generate from source — API / schema / CLI / envuser “b”
5Versioned snapshots (future, internal now)user “d”
6Dual audience — devs + cofounders (like the Google Drive cofounder doc)user “f”
7Write-time quality gates — lint / link-check / spell / styleuser “g”
8Diagram-as-code — Mermaid / D2, no PNG screenshotsuser “h”
9Living examples — code snippets executed in CIuser “i”
10Multi-repo federation — same framework for Ideony + Jooiceuser “j”

Choice: Diátaxis (taxonomy) + Starlight (renderer) + llms.txt (agent index) — hybrid D

Section titled “Choice: Diátaxis (taxonomy) + Starlight (renderer) + llms.txt (agent index) — hybrid D”

Taxonomy — Diátaxis (already adopted in docs/README.md). Four quadrants: tutorial, how-to, reference, explanation. No change.

RendererStarlight (Astro-based).

  • On-stack (TS / Vite ecosystem matches Expo + Nest)
  • Pagefind static search (no Algolia signup)
  • Content collections load raw MD from docs/ — no copy, no drift
  • Plugins native for every goal (starlight-openapi, starlight-llms-txt, starlight-versions, Mermaid via rehype-mermaid)
  • Cloudflare Pages free tier on existing ideony.is-a.dev zone

Agent indexllms.txt + llms-full.txt standard. Machine-readable doc index for LLMs. Auto-generated by starlight-llms-txt.

Reason not picked
Markdown-only (A)No search for cofounders; no API auto-render; no llms.txt out of box
Docusaurus (B)React hydration heavier than Astro; versioning+i18n better but YAGNI for MVP 0; aesthetics dated OOB
Mintlify (C)SaaS lock-in contradicts OSS-first lean; $150+/mo past free tier; limits on MDX + self-host
MkDocs MaterialPython stack off-tree (you’re TS everywhere)
VitePressVue-based; thinner plugin coverage for OpenAPI + llms.txt
NextraNext.js-shaped; you’re Expo + Nest, no Next in tree

Rationale will be moved to an ADR (docs/decisions/0027-docs-framework-starlight.md) on lock.


Invariant: docs/ = content SSoT. apps/docs/ = renderer shell (no content, ~8 files). Deleting apps/docs/ any day → docs still work on GitHub as raw MD. Starlight is a view, not a store.

apps/
docs/ # Starlight app (renderer only)
astro.config.mjs # plugins: llms-txt, openapi, mermaid, pagefind
package.json # astro + @astrojs/starlight + plugins
src/
content.config.ts # content collections → ../../docs loader
styles/custom.css # brand tokens (Italian Sole palette)
components/ # Header override, cofounder gate
public/ # favicon, OG image
docs/ # SSoT — raw MD, git-tracked, human+agent write here
README.md # sitemap (Diátaxis index, unchanged)
architecture.md # unchanged
design-system.md # unchanged
getting-started.md # unchanged
glossary.md # unchanged
infrastructure.md # unchanged
roadmap.md # unchanged
status.md # unchanged
testing.md # unchanged
decisions/ # ADRs, unchanged
specs/ # feature designs, unchanged
plans/ # impl plans, unchanged
research/ # audits, unchanged
archive/ # historical, unchanged
_generated/ # NEW — gitignored, CI-written
api/
openapi.md # from NestJS @nestjs/swagger
scalar.html # OpenAPI viewer
db/
schema.md # from Prisma schema
erd.mmd # Mermaid ERD
config/
env.md # from .env.example + annotations
cli/
pnpm-scripts.md # extracted from package.json scripts
CHANGELOG.md # stays at repo root
CLAUDE.md # stays at repo root

Why apps/docs/ and not docs-site/ or docs/.starlight/

Section titled “Why apps/docs/ and not docs-site/ or docs/.starlight/”
  • Matches monorepo apps/* convention (already used by apps/api + apps/mobile)
  • Turborepo pipeline picks it up free (docs:build task cached remote)
  • pnpm -F docs dev / pnpm -F docs build natural
  • Isolates Astro deps from root package.json (smaller root lockfile)
  • Jooice federation later — same apps/docs/ pattern reusable
  • No MD inside apps/docs/src/content/docs/ — loader reads from ../../docs/**
  • No copying docs/*.md into apps/docs/ — destroys SSoT
  • No hand-editing docs/_generated/** — CI-only

Each pipeline runs as a pnpm script, writes into docs/_generated/**, committed to gitignore. CI + dev both run pnpm docs:gen before Starlight build.

Source: NestJS @nestjs/swagger (already producing spec in apps/api/scripts/generate-openapi.js, 30 routes, pipes into packages/api-client/ SDK gen).

Pipeline:

  1. pnpm -F api generate:openapi → writes apps/api/openapi.json
  2. widdershins apps/api/openapi.json -o docs/_generated/api/openapi.md (or openapi-to-md)
  3. Scalar viewer: copy spec → docs/_generated/api/scalar.html (Scalar CDN script reads spec)
  4. starlight-openapi plugin renders spec into searchable route pages in Starlight

Reuses existing spec → zero new infra for API docs. Auto-updates on every API route change.

Source: prisma/schema.prisma.

Pipeline:

  1. prisma-markdowndocs/_generated/db/schema.md (tables, fields, relations, indexes)
  2. prisma-erd-generatordocs/_generated/db/erd.mmd (Mermaid)
  3. Starlight page embeds both via <Mermaid /> + frontmatter include

Source: .env.example (already annotated with comments).

Pipeline:

  1. Small node script parses .env.example, extracts # KEY — description comments + default values
  2. Emits docs/_generated/config/env.md as table (var | purpose | required | default | example)

Source: root package.json + per-workspace package.json scripts sections.

Pipeline:

  1. Node script aggregates all scripts from all package.json in workspace
  2. Cross-refs a docs/cli-descriptions.yml file (hand-maintained 1-line descriptions per script)
  3. Emits docs/_generated/cli/pnpm-scripts.md

4.5 TypeScript types → Markdown (optional, phase 2)

Section titled “4.5 TypeScript types → Markdown (optional, phase 2)”

Source: packages/types/src/**.

Tool: TypeDoc with typedoc-plugin-markdown.

Pipeline: typedoc --plugin typedoc-plugin-markdown packages/types/src/index.ts --out docs/_generated/types/.

Deferred — value lower than 4.1–4.4 for MVP 0.


All gates run in pre-commit (Husky + lint-staged) AND CI (.github/workflows/docs-quality.yml). Fail-fast.

GateToolScopeReason
Markdown lintmarkdownlint-cli2docs/**/*.md + CLAUDE.md + README.mdBroken tables, heading levels, inconsistent list markers
Prose stylevale + write-good + custom rulessameCatches hedging, passive voice, banned words (from CLAUDE.md: critical, crucial, essential, robust, elegant)
Spell checkcspell w/ project dictionary (Ideony, Dokploy, Clerk, Expo, NestJS, Gluestack, Mapbox, etc.)sameTypos in technical terms
Link checklychee (Rust, fast)all MD + ADRs + specsDead internal + external links; catches doc reorg breakage
Frontmatter schemacustom Ajv validator in scripts/validate-frontmatter.tsspecs + ADRs (they have Date: / Status: / Supersedes:)Schema drift on structured docs
Diátaxis tagcustom check in scripts/check-diataxis-quadrant.tsany doc in docs/ top-levelEach doc must declare quadrant in frontmatter
Mermaid syntax@mermaid-js/mermaid-cli dry-renderall .mmd + fenced mermaid blocksCatches unrenderable diagrams pre-merge
Code-block compileremark-typescriptfenced ts / tsx blocks marked exampleLiving examples (goal #9)

Mark code blocks with ts example language tag. Pre-commit hook extracts them → writes to tmp/examples/<file>-<hash>.ts → runs tsc --noEmit w/ project tsconfig. Fail = broken example.

For full runtime examples (e.g., API snippet that must compile + return 200), write them as e2e/docs-examples/*.spec.ts Playwright/Jest tests referencing the doc file. Drift fails in normal test run.


User flagged goal #2 (“review if accurate already”). Current state:

  • scripts/check-docs-updated.sh — pre-commit hook mapping source paths → required doc paths (policy in scripts/README-docs-gate.md)
  • Enforced in 3 layers: pre-commit, commit-msg, pre-push + CI workflow
  • Server-side branch protection OFF during MVP 0 (honor system, Claude is sole committer)
  1. Read current check-docs-updated.sh line-by-line
  2. Cross-check against CLAUDE.md trigger matrix (14 rows)
  3. For each row:
    • Does the gate cover it automatically? → OK
    • Does CLAUDE.md say “manually walk matrix”? → flag
  4. Gaps list goes into a follow-up task. Enhancement patterns:
    • Shipped-feature detection: if commit type is feat: → require CHANGELOG.md + docs/status.md + docs/roadmap.md ALL staged (currently gate only requires one)
    • ADR trigger: if docs/decisions/** unchanged + commit touches listed arch surfaces → prompt for ADR
    • Auto-gen freshness: if API routes changed but docs/_generated/api/openapi.md not regenerated → fail
  5. Upgrade check-docs-updated.sh to fill gaps; add tests in check-docs-updated.test.sh

Output: docs/research/2026-04-2x-docs-gate-audit.md + patch to the gate.


Section titled “7. Agent-native layer — llms.txt + search”

User explicit: agents (Claude) are primary doc consumers, humans second.

Two files at site root (served by Starlight):

  • /llms.txt — curated index: title, one-liner, link to each doc. Human-readable, 1–2KB.
  • /llms-full.txt — flat concatenation of all MD content, delimited by headings. 10s of KB, single fetch = full context.

Generated by starlight-llms-txt plugin. Zero hand-maintenance.

  • Canonical MD URLs — every Starlight page exposes ?format=md → returns raw source MD (Starlight native). Claude fetches https://docs.ideony.is-a.dev/architecture?format=md → gets MD, not HTML.
  • Stable anchors — enforce slug format (kebab-case, no auto-numbering) so LLM-generated links stay valid.
  • Frontmatter = agent schema — every doc declares quadrant, status, last_reviewed, audience (dev / cofounder / agent). Agents filter on audience: agent for the curated subset.
  • llms-ctx.md per complex topic — short curated brief per module (auth flow, Mapbox integration, SOS dispatch) surfaced first in llms.txt.
  • Pagefind (static, default in Starlight) — full-text, client-side, no infra, no API key, works offline. Good enough for MVP 0.
  • Phase 2 (optional): Kapa.ai / Inkeep for semantic search over doc corpus, $$/mo. YAGNI for now.

Hard ban: no PNG / JPG / screenshot-of-draw.io diagrams. Repo stores source, renders at build time.

Use caseToolOutput
Flowcharts, sequence, state, classMermaid (default)Rendered inline by rehype-mermaid at build; raw .mmd inspectable
Architecture / network / complex layoutsD2 (optional phase 2)SVG via d2 CLI
Pixel UX mockupsExcalidraw scene files (.excalidraw)Rendered via excalidraw-to-svg in CI

Current docs/architecture.md already has C4 block diagrams — migrate to Mermaid C4Context / C4Container diagrams. One commit, mechanical.


Goal #6: same docs serve devs + cofounders (replace the Google Drive Ideony doc over time).

Option picked: Cloudflare Access (Zero Trust) on docs.ideony.is-a.dev

Section titled “Option picked: Cloudflare Access (Zero Trust) on docs.ideony.is-a.dev”
  • Public perimeter: entire site behind Cloudflare Access
  • Access policy: email allowlist (cofounders + team Google Workspace) via CF Zero Trust
  • Free tier: 50 users (plenty for Ideony pre-launch)
  • Auth: one-time email PIN or Google OAuth — no passwords
  • Future: public subset via separate docs-public.ideony.is-a.dev domain pulling only audience: public tagged docs

Every doc frontmatter gets audience: enum: agent | dev | cofounder | public. Starlight build filters:

  • Main site: all
  • /cofounder/ subpath: audience: cofounder or audience: public only (hides internal tech detail)
  • /public/ (future phase 2): audience: public only

A migration pass moves shared decisions / roadmap / brand narrative from the cofounder Google Doc into docs/specs/ + docs/roadmap.md + a new docs/cofounder-overview.md (tagged audience: cofounder). Drive doc becomes historical / archive.


User: “yeah cool, for now only internal but maybe in future.”

MVP 0 → post-launch: single version (main)

Section titled “MVP 0 → post-launch: single version (main)”
  • No versioning pre-launch. Docs reflect current main.
  • starlight-versions plugin installed but inactive.

When a public API surface or app ships v1.0:

  • Tag v1.0 triggers docs snapshot: starlight-versions creates /v1.0/ subpath
  • Subsequent versions: /v1.1/, /v2.0/, etc.
  • /latest/ alias → current stable

Only docs/decisions/, docs/specs/, and the reference quadrant (architecture.md, design-system.md, API docs, schema docs) get versioned. Plans + research + archive stay unversioned (they’re historical by nature).


Goal #10. Design now, implement when Jooice ready.

  • Ideony apps/docs/ + Jooice apps/docs/ = identical Starlight shells
  • Shared packages/docs-shell/ (new workspace) contains astro.config defaults, shared styles, shared plugins config, shared cspell dict, shared vale styles
  • Third “hub” repo OR Ideony itself as hub — docs.ideony.is-a.dev/ideony/** + docs.ideony.is-a.dev/jooice/**, content pulled via Astro remote content collections

Pick hub model when Jooice’s docs framework lands. For now, structure Ideony apps/docs/ so packages/docs-shell/ extraction is a one-day refactor.


[ commit ] → [ pre-commit gates (quality, living-examples, zero-drift) ]
→ [ push ]
→ [ CI: docs-ci.yml
- pnpm install
- pnpm docs:gen # regenerate _generated/**
- pnpm docs:quality # rerun all gates
- pnpm -F docs build # Starlight static build
- upload dist/ artifact
]
→ [ CI: docs-deploy.yml (main-branch only)
- download dist/
- cloudflare-pages deploy
- smoke check: curl /llms.txt, /architecture, /api, /
]
  • Target: Cloudflare Pages, project name ideony-docs, custom domain docs.ideony.is-a.dev
  • Zone: existing ideony.is-a.dev (same as app. + api. subdomains)
  • Access: Cloudflare Zero Trust policy (email allowlist)
  • Cache: default CF edge cache; llms.txt / llms-full.txt w/ short TTL (1 hr) so agents get fresh context fast
  • Cost: €0 (Pages free tier: 500 builds/mo, unlimited bandwidth)

turbo.json adds:

{
"pipeline": {
"docs:gen": { "outputs": ["docs/_generated/**"] },
"docs:build": {
"dependsOn": ["docs:gen"],
"outputs": ["apps/docs/dist/**"],
"inputs": ["docs/**", "apps/docs/**", "packages/docs-shell/**"]
}
}
}

Remote-cached → CI rebuilds cheap.


Ship incrementally. Each phase independently valuable, reversible.

Parallelism: Phases 1 → 2 → 3 are sequential (4 depends on 3’s _generated/ outputs). Phases 5 + 6 run parallel (Q5 resolution 2026-04-21) — disjoint file sets. Phase 7 + 8 async / non-blocking. Phase 4.5 gated on Phase 4 clean-for-1-week.

  • Scaffold apps/docs/ with Starlight starter
  • Content loader pointing at ../../docs
  • docs/README.md → Starlight landing
  • Build passes locally, no plugins yet
  • Deploy to CF Pages behind Access (cofounder-visible)
  • Done when: docs.ideony.is-a.dev renders every existing docs/**/*.md
  • starlight-llms-txt/llms.txt + /llms-full.txt live
  • ?format=md raw source URLs
  • Frontmatter schema enforced on all docs/*.md (migration PR adds quadrant:, audience: to each existing file)
  • Done when: curl docs.ideony.is-a.dev/llms-full.txt returns full corpus

Phase 3 — Auto-gen pipelines (day 2–3)

Section titled “Phase 3 — Auto-gen pipelines (day 2–3)”
  • OpenAPI → MD (reuses existing Nest Swagger)
  • Prisma schema + ERD → MD
  • env → MD
  • pnpm scripts → MD
  • docs/_generated/ gitignored, CI-regen-before-build
  • Done when: docs/_generated/api/openapi.md renders 30 routes, stays in sync on every API commit
  • markdownlint, vale, cspell, lychee, frontmatter validator
  • Pre-commit + CI hooks
  • Husky lint-staged integration
  • Living-examples: ts example tag + extraction script wired, local dev-loop only (no CI block yet — Q4 resolution 2026-04-21)
  • Done when: CI blocks a commit introducing a dead link or banned word

Phase 4.5 — Living-examples CI block (day 5+, post-validation)

Section titled “Phase 4.5 — Living-examples CI block (day 5+, post-validation)”
  • Promote local extraction → CI gate once Phase 4 has run clean ≥ 1 week w/ zero false positives
  • Add tsc --noEmit step to docs-quality.yml
  • Done when: CI fails a commit that breaks a ts example block

Phase 5 — Zero-drift audit + upgrade (day 3–4)

Section titled “Phase 5 — Zero-drift audit + upgrade (day 3–4)”
  • Read check-docs-updated.sh, cross-check CLAUDE.md trigger matrix
  • Report gaps in docs/research/
  • Patch gate, add tests
  • Done when: every row in CLAUDE.md trigger matrix is gate-enforced, not honor-system
  • Convert C4 blocks in architecture.md to Mermaid C4Context
  • Any other PNG → .mmd / .d2
  • Done when: zero image files in docs/** outside logos

Phase 7 — Cofounder migration (day 5+, async, non-blocking)

Section titled “Phase 7 — Cofounder migration (day 5+, async, non-blocking)”
  • Extract Google Drive cofounder doc content → docs/cofounder-overview.md + relevant specs
  • Tag audience: cofounder
  • Share docs.ideony.is-a.dev link with cofounders
  • Drive doc stays live in parallel (Q3 resolution 2026-04-21). Sunset only when cofounders confirm they’re reading the site; no hard cutoff.

Phase 8 — Federation prep (post-Jooice docs init)

Section titled “Phase 8 — Federation prep (post-Jooice docs init)”
  • Extract shared config → packages/docs-shell/
  • Document federation recipe in docs/docs-framework.md

RiskMitigation
Starlight abandonwareContent is raw MD — Starlight removable in one PR, switch to Docusaurus/MkDocs
llms.txt standard doesn’t take offllms-full.txt is just concatenated MD — still useful even if spec stalls
Auto-gen pipelines break on schema changeCI gates: docs:gen failure blocks merge
Cloudflare Access misconfig exposes internal docsStart closed (default deny); add emails one-by-one; audit log on Zero Trust
Devs write MD, bypass frontmatter schemaPre-commit validator + PR template checklist
Lockfile churn on pnpm-lock.yaml during multi-agent phasesImpl in git worktree; merge via format-patch when parallel agents idle
Living-examples TypeScript driftExtracted examples hit same tsc as source → if source compiles, example compiles

  • Kapa.ai / Inkeep semantic search (phase 2+)
  • Public docs subdomain (phase 2+)
  • i18n translations (IT/EN auto-sync) — future, not MVP 0
  • Jooice docs stand-up (Jooice owns that)
  • Storybook integration for component docs (apps/mobile design system has its own path)
  • Video embedding / Loom integration
  • Algolia DocSearch application (Pagefind covers MVP 0)

#QuestionAnswer
1apps/docs/ namingConfirmedapps/docs/
2Subdomain vs pathSubdomain docs.ideony.is-a.dev — SOTA industry standard (Vercel, Stripe, Clerk, Supabase, Mapbox, Cloudflare all use docs.*). Better Cloudflare Access isolation, separate CDN caching, cleaner llms.txt origin.
3Cofounder accessBoth during migrationdocs.ideony.is-a.dev w/ CF Access + Drive doc stays in parallel. Drive sunset trigger deferred: once docs/cofounder-overview.md ships in Phase 7 AND cofounders confirm they’re reading the site. No hard cutoff.
4Living examplesImplement syntax + extraction now, CI enforcement later. Phase 4 scaffolds ts example tag + extraction script (local-only). CI block on failed compile added in a Phase 4.5 after dev-loop validates no false positives.
5Phase 5 vs Phase 6 orderingParallel. Phase 5 (gate audit) = scripts + research doc, independent file set. Phase 6 (diagram migration) = MD edits in docs/architecture.md. Zero overlap → parallelize to compress timeline.

All five resolved — spec cleared for Phase 1 kick-off.