← All docs

Document management — architecture & design

How Sponic manages documentation so that both humans and AI agents can find, read, and reason over the same source of truth without drift.
2026-05-04 RFC / Proposal ~5 day implementation Touches: MkDocs · Supabase · pgvector · CF Pages · GitHub Actions

Key idea

docs/ in git is the only thing humans or agents write to. Everything else — the pretty site, the vector index, the search API — is derived state, regenerated by the pipeline. If the DB explodes we rebuild from git. There is exactly one source of truth.

Contents

  1. Goals & non-goals
  2. The shape of the answer
  3. Storage layer — markdown + frontmatter
  4. Human layer — browse, navigate, search
  5. Machine layer — API for agents
  6. Information processing — timing
  7. Design decisions — explicit tradeoffs
  8. What we're NOT building yet
  9. Open questions
  10. Implementation order
  11. Appendix A — SSG deep comparison
  12. Appendix B — Why not Obsidian?

1. Goals & non-goals

Goals

Non-goals

2. The shape of the answer

┌─────────────────────────────────────────────────────────────────┐ │ SOURCE OF TRUTH │ │ │ │ docs/ ← markdown files, committed to git │ │ ├─ partners/ frontmatter carries structured │ │ ├─ runbooks/ metadata │ │ ├─ charter/ │ │ ├─ devtasks/ │ │ └─ assets/ ← images alongside docs │ └─────────────────────────────────────────────────────────────────┘ │ │ │ git push │ nightly cron ↓ ↓ ┌──────────────────────┐ ┌──────────────────────────────┐ │ HUMAN LAYER │ │ MACHINE LAYER │ │ sponicgardens.com/ │ │ Postgres (Supabase) + │ │ docs │ │ pgvector │ │ │ │ │ │ • MkDocs Material │ │ • docs table (frontmatter │ │ • full-text search │ │ + body + html_render) │ │ • tag pages │ │ • doc_chunks table │ │ • backlinks │ │ (chunk + embedding) │ │ • image gallery │ │ • doc_links (graph) │ │ • reading mode │ │ • doc_search_index (FTS) │ │ │ │ │ │ Static, CDN-cached │ │ REST API for agents: │ │ on Cloudflare Pages │ │ /api/docs/search │ └──────────────────────┘ └──────────────────────────────┘

3. Storage layer — markdown + frontmatter

Directory layout

docs/
├── README.md                      # entry point
├── _meta/                         # taxonomies, tag definitions, glossary
│   ├── tags.yml
│   └── glossary.md
├── charter/                       # mission, values, governance
├── partners/                      # one file per partner org
│   ├── most.md
│   └── bujna-warszawa.md
├── locations/                     # one file per location/space
├── people/                        # one file per person
├── runbooks/                      # operational recipes
├── brand-kit/                     # brand decisions, voice, imagery rules
├── devtasks/                      # active eng/design work
├── decisions/                     # ADRs
└── assets/                        # images, diagrams, PDFs
    └── <doc-slug>/                # each doc's images in its own folder

Frontmatter schema

Every .md file has YAML frontmatter — the structured metadata that powers filtering, faceting, and graph navigation.

---
id: doc_partners_most                  # stable, never changes
title: MOST — Mazovia Open Spaces Trust
slug: most                             # url-safe, may differ from id
type: partner                          # partner | location | person | runbook | adr | charter | devtask
status: active                         # draft | active | archived
tags: [warsaw, urban-gardens, ngo]
links:                                 # explicit, typed cross-references
  - { rel: location, target: doc_locations_warsaw }
  - { rel: contact,  target: doc_people_jane_kowalski }
created_at: 2026-04-12
updated_at: 2026-05-04
updated_by: agent:partner-research-bot # or human:rahul
review_cadence: quarterly              # how often this should be re-checked
summary: >
  One-paragraph machine-readable summary. Used as the embedding
  source for "topic-level" semantic search. Keep under 500 chars.
---

Why this matters

Why markdown specifically

4. Human layer — browse, navigate, search

The human-facing site is a static MkDocs Material site built nightly (and on every push) and deployed to Cloudflare Pages.

Browse — visual hierarchy

Navigate — graph structure

Search — three modes, one box

The search box does three things in parallel, merging results:

  1. Full-text (lunr/BM25) — runs entirely in the browser, instant. Indexes title, headings, body, and frontmatter tags.
  2. Semantic (pgvector) — sends the query to /api/docs/search, returns top-N semantically similar chunks. Slower (~200–500ms) but catches concept matches.
  3. Faceted filter — sidebar filters: type, tag, status, updated-since. Stackable on top of either search mode.

Reading mode

5. Machine layer — API for agents

The agent-facing API lives in apps/control (already a Next.js project on in.sponicgardens.com). It reads from a Postgres database that mirrors docs/ after each pipeline run.

Schema

-- one row per doc; mirrors a single .md file
create table public.docs (
  id              text primary key,
  slug            text unique not null,
  type            text not null,
  title           text not null,
  status          text not null,
  summary         text,
  tags            text[] not null default '{}',
  body_markdown   text not null,
  body_html       text not null,
  frontmatter     jsonb not null,
  path            text not null,
  git_sha         text not null,
  updated_at      timestamptz not null,
  updated_by      text not null,
  word_count      int not null,
  fts             tsvector generated always as (
                    setweight(to_tsvector('english', title), 'A') ||
                    setweight(to_tsvector('english', coalesce(summary, '')), 'B') ||
                    setweight(to_tsvector('english', body_markdown), 'C')
                  ) stored
);

-- chunks for fine-grained semantic search
create table public.doc_chunks (
  id          bigserial primary key,
  doc_id      text not null references public.docs(id) on delete cascade,
  chunk_idx   int not null,
  heading     text,
  content     text not null,
  token_count int not null,
  embedding   vector(1536) not null,
  unique (doc_id, chunk_idx)
);

-- typed graph between docs
create table public.doc_links (
  source_id text not null references public.docs(id) on delete cascade,
  target_id text not null references public.docs(id) on delete cascade,
  rel       text not null,
  primary key (source_id, target_id, rel)
);

Agent API — one endpoint, many modes

POST /api/docs/search

{
  "q": "partners doing urban gardens in warsaw",
  "mode": "hybrid",          // fts | semantic | hybrid (default)
  "filters": {
    "type": ["partner", "location"],
    "tags": ["warsaw"],
    "status": "active",
    "updated_since": "2026-01-01"
  },
  "include": ["summary", "tags", "body"],
  "limit": 10
}

Returns:

{
  "results": [{
    "id": "doc_partners_most",
    "slug": "most",
    "type": "partner",
    "title": "MOST — Mazovia Open Spaces Trust",
    "score": 0.87,
    "match_reasons": ["fts:warsaw", "semantic:urban-gardens"],
    "summary": "...",
    "tags": ["warsaw", "urban-gardens", "ngo"],
    "snippet": "...highlighted excerpt..."
  }],
  "total": 3,
  "took_ms": 142
}

Companion endpoints:

Why one search endpoint?

Agents shouldn’t need to know whether their question is best answered by FTS, vectors, or filters. We make that decision server-side and tell them which signals fired (match_reasons).

6. Information processing — when does each step happen?

Three timing tiers: upfront (one-time setup), on every commit (CI), nightly (cron). Plus one on-demand mode for agents writing docs in real time.

6.1 Upfront (one-time, ~1–2 days of work)

6.2 On every commit to main (~1–3 min)

GitHub Actions docs-pipeline workflow:

  1. Lint — validate every .md file has required frontmatter fields; check that id is unique; check that all links.target IDs exist.
  2. Build sitemkdocs build. Outputs static HTML to site/.
  3. Diff — compare git diff HEAD~1 HEAD -- docs/ to find changed files.
  4. Index changed docs — for each changed file: parse frontmatter + body, upsert into public.docs, re-chunk, re-embed via text-embedding-3-small, rebuild doc_links rows.
  5. Deploy — push site/ to Cloudflare Pages.

This is the fast path. Agents committing one file get their changes searchable in ~90 seconds.

6.3 Nightly (~3 AM, ~10–30 min)

GitHub Actions docs-nightly workflow:

  1. Full reindex audit — recompute embeddings + chunks for any doc whose git_sha in DB doesn’t match the current commit. Catches drift.
  2. Stale detection — flag docs whose updated_at + review_cadence < now(). Surfaces a “Needs review” banner on the site.
  3. Broken-link sweep — crawl the rendered site for 404s in internal links and image references; open a GitHub issue listing any found.
  4. Agent-driven updates — invoke registered “doc-updater” agents (e.g. partner-research-bot, runbook-syncer). Each reads its assigned docs, decides if updates are needed, and commits changes.
  5. Search-quality eval — replay canned queries against the search API; alert if recall@5 drops below threshold.
  6. Backup — dump docs.* tables to R2 alongside the existing nightly DB backup.

6.4 On-demand (agent writing in real time)

6.5 The four pipeline scripts

apps/control/scripts/docs-pipeline/
├── parse.ts          # md + frontmatter → typed Doc object
├── index.ts          # Doc → upsert into public.docs
├── embed.ts          # Doc → chunks → embeddings → doc_chunks
└── link-graph.ts     # Doc → doc_links rows

Each script is callable independently (--doc <path>) and collectively from a --all orchestrator.

7. Design decisions — explicit tradeoffs

D1. Markdown + git over a wiki app

Chose: flat files in git.

Tradeoff: No fancy editor; humans need VS Code or web editor. In return: zero vendor lock-in, perfect diffability, free version control, agents write to it natively, no per-seat cost.

D2. MkDocs Material over Docusaurus / Astro Starlight

Chose: MkDocs Material.

Tradeoff: Python toolchain (we’re otherwise mostly Node/TS). In return: fastest time-to-pretty-site, best out-of-box search, best plugin ecosystem. Migration to a JS-native SSG later is a 1-day job since markdown is portable.

D3. Postgres + pgvector over a dedicated vector DB

Chose: pgvector in our existing Supabase instance.

Tradeoff: Not best-in-class for billion-scale embeddings. In return: zero new infrastructure, free joins between docs and the rest of our schema, one backup story. We have ~thousands of docs, not billions of vectors.

D4. Hybrid search by default (FTS + semantic)

Chose: server-side hybrid with match_reasons exposed.

Tradeoff: Slightly more code. In return: robust to both keyword queries (“warsaw”) and conceptual queries (“partners doing community gardens”). Pure vector misses exact keywords; pure FTS misses synonyms.

D5. Frontmatter graph over a separate graph DB

Chose: typed links: array in frontmatter, materialized into a doc_links table.

Tradeoff: No Neo4j-style multi-hop query language. In return: the graph lives next to the prose, diffs cleanly, no extra infrastructure. SQL recursive CTEs handle 2–3 levels fine.

D6. Nightly full reindex on top of fast-path commit indexing

Chose: both, with nightly as a self-healing audit.

Tradeoff: A bit of redundant work each night. In return: if the fast path ever drops a message, eventual consistency is restored within 24h. Nightly cost is dollars per month.

D7. Stable id separate from slug

Chose: every doc gets an immutable id (doc_<type>_<name>) plus a renamable slug for URLs.

Tradeoff: Two identifiers. In return: when a partner changes its name and we rename the doc, every existing link keeps working because they reference the id.

8. What we're NOT building yet

9. Open questions for review

  1. Site location: docs.sponicgardens.com (public) or in.sponicgardens.com/docs (gated intranet)? Default proposal: intranet, since most content is internal.
  2. Embedding model: text-embedding-3-small (cheap, good) vs text-embedding-3-large (better, 6× cost)? Default proposal: small, with a documented upgrade path.
  3. obsid-sponic/ promotion: Automatic or manual? Default proposal: manual, so Sonia can keep her scratch space scratchy.
  4. Image handling: Embed via ![](r2-url) in markdown, or add an images: array to frontmatter? Default proposal: frontmatter array, so machines can find images per doc without parsing prose.

10. Implementation order

Day 1 — Scaffolding

docs/ skeleton + frontmatter schema + validator. mkdocs.yml + first deploy of the empty site. Postgres migration for docs, doc_chunks, doc_links.

Day 2 — Pipeline

Four pipeline scripts (parse, index, embed, link-graph). GitHub Actions workflow for the on-commit fast path. /api/docs/search MVP (FTS only).

Day 3 — Semantic + UX

Embedding pipeline + hybrid search. MkDocs theme polish, tag pages, backlinks plugin. Migrate first batch of real docs (charter + runbook).

Day 4 — Nightly + agent integration

Nightly workflow (audit, stale, broken-links, search-quality eval). POST /api/docs/{id}/note for agent-appended observations. Agent API documentation.

Day 5 — Close the loop

Migrate remaining canonical docs. Hook up partner-research-bot and runbook-syncer to the nightly agent slot. Cut over from any old doc-storage habits.

Appendix A — Static site generator deep comparison

The choice between MkDocs Material, Docusaurus, and Astro Starlight is the single biggest UX decision. All three render markdown to a fast, searchable site; they differ in toolchain, machine-friendliness, and feature ceiling.

A.1 At a glance

AttributeMkDocs MaterialDocusaurusAstro Starlight
LicenseMIT (Insiders tier paid)MITMIT
License cost$0 community / ~$15/mo Insiders$0$0
Hosting cost$0 (CF Pages)$0 (CF Pages)$0 (CF Pages)
ToolchainPython (pip)Node (Yarn/npm)Node (npm/pnpm)
FrameworkJinja2 templatesReactAstro (islands)
Build speed (1k pages)~5s~25s~10s
Outputstatic HTMLstatic HTML + React hydrationstatic HTML + minimal JS
Markdownyes (Python-Markdown)yes (MDX = MD + JSX)yes (Markdown + MDX)
Frontmatteryes, generic YAMLyes, typed via pluginsyes, Zod-validated
Searchexcellent (lunr, offline)needs Algolia or pluginbuilt-in (Pagefind)
Tags / tag pagesbuilt-in pluginDIYDIY
Backlinkscommunity pluginDIYDIY
Mermaid diagramsone-line configone-line configplugin
Image lightboxglightbox pluginDIYDIY
Git historyplugin, freeplugin, freeplugin
Theming flexibilitymediumhigh (React)very high (full Astro)
i18nyesyes (best)yes
Versioned docsyes (mike)yes (best)yes
Maturity2014, very stable2017, very stable2023, fast-moving
GitHub stars~22k + ~22k~58k~7k (~50k Astro)
Time to “good site”30 min2–3 hrs1–2 hrs
Time to exact fit1–2 days2–4 days3–5 days

A.2 Machine-friendliness

This is the dimension that matters most for us, since Claude/agents will be the primary writers.

MkDocs Material

Bottom line: very high. Python toolchain is the only friction.

Docusaurus

Bottom line: medium. MDX-by-default is the wart for agent-driven authoring.

Astro Starlight

Bottom line: very high — if we accept fewer existing plugins.

A.3 Cost breakdown (3-year TCO)

Assumed scale: ~2,000 docs, ~5 GB of images, internal-only audience (<100 readers/day).

Cost lineMkDocs MaterialDocusaurusAstro Starlight
License$0 (or $15/mo)$0$0
Hosting$0 (CF Pages)$0 (CF Pages)$0 (CF Pages)
Search$0 (lunr)$0–50/mo Algolia$0 (Pagefind)
Build minutesnegligiblesmallsmall
Setup time~0.5 day~1.5 days~1.5 days
Yearly maintenance~1 day~3 days~2 days
3-year TCO~$4.2k~$12k~$8.4k

All three are free to run. The difference is engineer-time TCO, where MkDocs Material wins by being boring.

A.4 Weighted decision matrix

DimensionWeightMkDocsDocusaurusStarlight
Agent authoring (no MDX traps)★★★312
Time to “good enough”★★★322
Built-in features★★322
Theme/layout flexibility233
Programmatic injection★★233
Frontmatter schema validation★★123
Toolchain alignment (Node-heavy)133
Maintenance burden★★322
Plugin ecosystem maturity331
Weighted total413338

A.5 Recommendation

MkDocs Material narrowly wins on the weighted matrix and decisively wins on time-to-value. The two hits against it — Python toolchain and weaker frontmatter validation — are both manageable.

Astro Starlight is the close second and the right pick if we ever want a fully custom theme. The unified TS toolchain + Zod-validated frontmatter is genuinely lovely for agent-driven work. Migration to Starlight is a 1–2 day job because all content is portable markdown.

Docusaurus is the wrong pick for us specifically because of MDX defaults. MDX is a great format for documentation that includes React components; for documentation written by agents, it’s a liability.

Acceptance criteria (1-day spike)

Appendix B — Why not Obsidian as the canonical store?

We already use Obsidian for obsid-sponic/ (Sonia’s BD research scratch space). Obsidian is excellent for one-human-thinking-out-loud, but as the canonical doc system it has structural problems:

Best of both worlds

In the proposed architecture, anyone who likes Obsidian can point it at docs/ and get all of Obsidian’s editing/linking/graph features for free — because the source is just markdown files. We just don’t depend on Obsidian for the parts that need to be programmatic.