01 — The thesis
"Code is not cheap."
Microapp follows Matt Pocock's framing for AI-era software: software fundamentals matter more than ever, not less. Bad code makes AI worse — it amplifies the chaos. Good code lets AI do its best work.
A bad codebase is one you can't change without causing bugs (Ousterhout). A good codebase resists entropy — every commit either keeps it tidy or makes it tidier (Pragmatic Programmer / Kent Beck).

The bargain
02 — Stack
Static-first, edge-served, type-safe.
- Astro 5 SSG — every page
prerender = true, output"static". No Worker invocation per request. - React 19 islands — only the interactive widgets hydrate. Tool widgets
client:load; AuthModal / LanguageSwitcherclient:idle. - Cloudflare Pages — CDN-edge serving from
dist/. - Supabase — tool metadata + i18n translations + ratings. Build-time read only; the browser never queries directly.
- Tailwind 4 via
@tailwindcss/vite. Brand tokens live in@theme. - TypeScript strict.
astro checkruns voluntarily today; CI gate is on the backlog.
03 — Ubiquitous language
Same word — same meaning.
Pocock's #2 failure mode: AI talks past you because you don't share a vocabulary. We use these terms consistently across code, conversations, and this document. If a term is missing here and you need it, add it before using it in code.

| Term | Definition |
|---|---|
slug | Kebab-case canonical identifier, no leading slash. word-counter, bmi-calculator. The English slug is canonical for a tool across all locales. |
localized slug | Translated slug used in non-English URLs. contador-palabras for es. Lives in src/i18n/slug-mappings.ts (auto-generated). |
tool | One entry in src/lib/tools-data.ts — slug, label, desc, category, keywords, optional featured. |
widget | The interactive React component for a tool. Lives in src/components/tools/<PascalName>.tsx. ToolIsland auto-discovers via Vite glob. |
engine config | Config in src/lib/tool-registry.ts for Tier 1 converters/calculators that don't need a custom widget — rendered by <ToolEngine>. |
category | One of 7 fixed categories. Canonical record in src/lib/categories.ts; URL is /categories/<category-slug>. |
featured | Boolean flag on a tool. true = appears on the home page hero grid (~18 tools). The rest reach via /categories and search. |
locale | One of 7 codes (en, ru, es, hi, ar, de, pl). en is default and uses no prefix; others use /<locale>/... |
translation | A row in Supabase tool_metadata_translations keyed by (slug, locale). NOT the same as i18n UI strings. |
i18n strings | UI text translations bundled into the JS. Source: src/i18n/locales/<lang>/{common,home,tool,ratings}.ts. |
deep module | A component or function with a small interface and complex internals hidden inside (Ousterhout). We actively prefer these. |
04 — Architecture
Build at home. Serve at the edge.
Tool metadata lives in Supabase. Astro's getStaticPaths() reads it at build time and emits one static HTML file per (tool × translated locale). Cloudflare Pages serves those files from its CDN — no Worker, no per-request compute, no per-request cost.

Each request hits a static file. Inside that file, the only JavaScript that runs is the tool's interactive widget (a React island) — and that hydrates only after first paint.
What's NOT in the architecture
2b167d1) — switching back means installing @astrojs/cloudflare and changing one config line, but only if a server route actually needs to exist.05 — Deep modules
Small interface. Complex internals. Hidden.
Ousterhout: "Modules should be deep." A simple, narrow interface with rich behavior behind it. The opposite — many shallow modules — forces every reader (you, AI, future-self) to navigate the whole graph just to understand any part.

Our deep modules
getAllToolTranslations()Interface:() => Promise<Record<slug, Record<locale, ...>>>. Internals: one Supabase query, memoized at module scope, error-tolerant. Used by 3 routes; nobody reaches in.<ToolIsland slug={…} />Interface: one prop. Internals: Vite glob auto-discovery, slug aliasing for legacy URLs, Tier 1 engine dispatch. Adding a tool = drop a file insrc/components/tools/.<ToolLayout tool ... />SEO chrome + slots (tool-widget / faq-accordion / ratings). Caller passes data; layout emits HTML + locale-aware JSON-LD.<HomeIsland lang translations />Renders the curated home page. Pure render — no fetching, no client state. The 18 featured tools come fromtools-data.ts, the visuals fromTOOL_VISUALS.- Brand design system (
@themetokens)Interface:bg-brand-green,font-display, etc. Internals: hex values + font stacks, defined once insrc/styles/global.css. Change a brand color in one place; ~70+ consumers update.
06 — Sources of truth
Each piece of data lives in exactly one place.
The hardest design problem on this codebase has been: where does this piece of information live? We've answered it for "tool"; "category" is partly fragmented and on the backlog.
| File / table | Owns | Why separate |
|---|---|---|
src/lib/tools-data.ts | Canonical list of tools (slug, label, desc, category, featured) | Single TS source; bundle-friendly |
src/lib/categories.ts | The 7 fixed categories | Imported by category hub pages and ToolLayout nav |
src/lib/tool-registry.ts | Engine configs (Tier 1) | Engine logic = JS functions; can't live in a DB |
src/components/tools/*.tsx | Tool widgets (Tier 2/3) | One file per tool. ToolIsland glob auto-discovers |
TOOL_VISUALS in HomeIsland | Per-featured-tool icon + colors | Visual-only concern; build-time invariant catches drift |
Supabase tool_metadata | English SEO copy (title, intro, FAQs, etc.) | Long-form content + ratings live in DB |
Supabase tool_metadata_translations | Per-locale translated SEO copy | Translations would bloat the bundle |
src/i18n/locales/<lang>/ | UI string translations | Bundled — needed at render time |
src/i18n/slug-mappings.ts | English → localized slug per locale | Auto-generated; don't hand-edit |
Hard rule
07 — How to add a tool
Today: 7 places. Goal: 1.
The current recipe. Backlog item #4 is collapsing this into a pnpm new-tool <slug> scaffold script.
1. Create the widget src/components/tools/MyToolName.tsx
2. Add to canonical list src/lib/tools-data.ts (one ToolEntry)
3. (Optional) flag for home grid featured: true in tools-data
4. (If featured) add visual entry TOOL_VISUALS["/my-tool"] in HomeIsland.tsx
5. Add SEO copy in Supabase INSERT into tool_metadata
6. Add per-locale translations INSERT into tool_metadata_translations
7. Re-run slug-mappings generator scripts/generate-slug-mappings.mjs- Skip step 5 → page 404s (no
getStaticPathsrow). - Skip step 4 with
featured: true→ build fails with the TOOL_VISUALS invariant.
08 — Decisions made
Don't re-derive these.
Each of these is a closed question. Reopen only with a specific reason and the room to commit either way.
Why output: "static"?
No route uses server runtime. Cloudflare Worker invocations cost latency and money for nothing. Reinstall @astrojs/cloudflare and switch to "server" if a real server route is added later.
Why translated slugs (/es/contador-palabras, not /es/word-counter)?
Strong organic SEO in non-English markets. Yandex prefers Cyrillic Russian slugs even with the URL-encoding ugliness — the ranking signal beats the friction.
Why featured-only home page (~18 tools, not all 115)?
Concentrates internal-link equity, gives home a clear editorial intent, removes a 130-line drift surface. Long-tail discovery moves to /categories/<slug> and the search palette.
Why tools-data.ts as canonical, not Supabase?
Typed, version-controlled, no build-time DB dependency for the basic listing. Supabase owns long-form SEO copy and translations only.
Why drop locale × tool pages with no translation?
Duplicate-content risk. Untranslated users land on the canonical English URL; the locale URL simply doesn't exist.
Why batch translation fetch (getAllToolTranslations)?
Was 576 sequential awaits in getStaticPaths. Now one query memoized at module scope. Build time dropped from minutes to seconds.
Why @theme tokens, not Tailwind config?
Tailwind 4 wires CSS variables and utility classes from one @theme block. Inline-style sites can use var(--color-brand-green); Tailwind sites use bg-brand-green. Single source.
09 — The agent team
Three skills. One runner. Any frontend.
Microapp ships with three named agents that handle the build pipeline. Each is a SKILL.md file (plus templates and checklists) under .claude/skills/. The runner — a thin TypeScript loop using the Vercel AI SDK — turns those skills into something callable from any frontend: Claude Code, Slack, GitHub Actions, Manus, your laptop CLI.
Research
Steph
Find what to build next. Ahrefs + GSC + competitor diff, scored by volume × difficulty × intent × buildability × novelty. Flags which picks need Kai.
Inputs: tools-data.ts, GSC, Ahrefs
Product
Kai
Spec for non-trivial tools — required features, edge cases, acceptance criteria. Decides per-locale whether Simon should translate. Refuses to over-spec trivial tools.
Inputs: Steph's slug + Ahrefs SERP + competitor pages
Build
Bob
Ship a tool against Kai's spec (when present). Generates widget, wires catalog, auto-runs Supabase seed, runs gate, opens PR. Refuses out-of-scope.
Inputs: Spec at data/specs/<slug>.md, or just slug
Writer
Lace
Long-form English SEO article (1500–2500 words). Picks template by archetype, applies brand voice, validates against checklist, verifies examples against the actual tool.
Inputs: Tool metadata + Bob's widget source
Translate
Simon
Translate Lace's article + metadata into the locales Kai marked `yes`. Adapts keywords per locale (not literal). Writes Supabase rows + regenerates slug-mappings.
Inputs: Lace's English + Kai's translation block
QA
Ben
Test Bob's PR against everything: adversarial inputs, live preview, a11y audit, brand consistency, cross-locale smoke (each Simon-translated URL).
Inputs: PR diff + preview URL + spec
The pipeline runs sequentially with humans-in-the-loop at the boundaries: Steph reports → human picks one → (if non-trivial: Kai writes spec → human reviews) → Bob builds + auto-seeds Supabase → CI runs (vitest + astro check) → Cloudflare builds preview → Lace drafts the English article → (if Kai flagged any locales `yes`: Simon translates) → Ben tests preview against the spec, including each translated URL → human reviews and merges. The chain stays human-supervised on purpose until trust accumulates.
The portable runner — why we don't lock to Claude Code
Skills are data; the runtime is the consumer. Each skill is plain Markdown — model-agnostic, runtime-agnostic. The runner at agents/lib/runner.ts loads a SKILL.md, hands it to whichever model is configured (Claude Opus 4.7 by default, swappable to GPT-5 / Gemini / local Llama with one config line), and runs the tool-use loop.
Same engine, many frontends:
agents/
├── lib/
│ ├── runner.ts # the agent loop (Vercel AI SDK)
│ ├── tools.ts # bash, read, write, edit, fetch
│ └── providers.ts # default Claude; swappable
└── cli.ts # pnpm agent <name> <prompt>
.claude/skills/
├── bob/SKILL.md ← same skills, every runtime
├── ben/SKILL.md
├── steph/SKILL.md
└── seo-article/SKILL.md
Frontends — all call the same runner:
• Claude Code /bob slug-here
• CLI (any host) pnpm agent bob slug-here
• GitHub Actions node agents/cli.ts bob ${{ inputs.slug }}
• Slack bot on /bob → invoke runner → post result back
• Manus imports the same SKILL.md files
• Cron (weekly) pnpm agent steph "weekly report"Editing a skill in this repo propagates to every frontend the next time the runner is invoked. No upload, no sync, no drift. Adding a new agent is one new .claude/skills/<name>/SKILL.md; the CLI auto-discovers it.
Refusal is a feature
Foundation behind the agents (already in place):
tests/tools.test.ts— 27 vitest cases catch slug drift, transformer regressions, catalog integrity. Runs in CI on every PR.pnpm exec astro check— full TypeScript across 250+ files. Runs in CI. Currently 0 errors.scripts/gsc-pull.mjs+scripts/steph-research.mjs— data pipelines that the agents consume.SLUG_ALIASESmap +TOOL_VISUALSinvariant — guard rails that fail loud.
Open work for full autonomy: wire GitHub Actions triggers (weekly Steph cron, comment-triggered Bob, on-PR Ben), wire a Slack bridge, and pick an analytics provider so the agents can measure their own impact. None of that requires touching the agents themselves — only adding new frontends to the same engine.
10 — Open backlog
Deliberate non-decisions.
Items here are deferred on purpose. Each has a reason. Don't re-propose without reading; do propose if the priority should change.
- "Category" ubiquitous-language fix. categoryHref + categoryLabel + category (id) + CategoryMeta.slug + .anchor are 5 fields meaning related-but-different things. Same problem we fixed for 'tool' — do it for category.
- Wire agent frontends. GitHub Actions for cron-Steph + comment-Bob + on-PR-Ben, plus a Slack bot bridge. Agents don't change; only triggers do.
- Migrate the 114 tool widgets to brand tokens. Each widget has 0–7 brand-hex literals. Independent, low-risk. Mop up in one sweep when visual consistency matters.
- pnpm new-tool <slug> scaffold script. Collapse the 7-place tool-add into one command.
- Tier 1 engine i18n. <ToolEngine> renders English unit labels everywhere. Defer until non-English traffic shows demand.
- RTL verification for Arabic. dir='rtl' is plumbed; visual flip needs a browser audit.
- Per-tool dynamic OG images. Single static og-image.png today; build-time generate per-tool with satori.
- Service worker. Instant tool-to-tool nav (@vite-pwa/astro).
- Replace main with astro-migration. Once we're confident in this rewrite. Old SPA is feature-frozen.
11 — Pocock score
Honest current state vs the framework.
Where the codebase sits today against Pocock's six failure modes. Aspirational, not promotional.
| Dimension | Score | Note |
|---|---|---|
| Render pipeline (page → layout → island) | 8/10 | Each layer hides complexity from the next; deep. |
| Tool widget pattern (glob auto-discovery) | 9/10 | Drop a file, it works. Textbook deep module. |
| Tool data layer | 7/10 | tools-data.ts is canonical now; 7-place tool-add is the gap. |
| Design system (brand tokens) | 7/10 | Foundation in place; 114 tool widgets still inline. |
| Layouts (ToolLayout / HomeNav / etc.) | 6/10 | Inline styles gone; could split into smaller modules. |
| Category data layer | 4/10 | 5 fields meaning related things — backlog. |
| Translation system | 5/10 | Two parallel implementations (i18n strings vs Supabase translations). |
| Build feedback / invariants | 9/10 | vitest + astro check both gating in CI; TOOL_VISUALS + slug-roundtrip + transformer regressions all asserted. |
| Documentation / shared design concept | 8/10 | AGENTS.md + CLAUDE.md + this page. |
| Agent infrastructure | 7/10 | Steph/Bob/Ben skills + portable runner; frontends (Slack, GitHub Actions cron) still manual. |
| Weighted overall | ~7/10 | Up from ~3/10 at the start of the design-investment work. |
The trajectory matters more than the number