The first version of microapp.io ran on a single-page React app with client-side routing.
Every page on the site shipped a 280-kilobyte JavaScript bundle that booted before the user saw their first calculator. The PageSpeed score on the home page was 54. The Time to Interactive was four seconds on a fast cable connection and twelve seconds on the kind of LTE the bakery owner reads the site on. The architecture was correct for an app — Microapp isn't an app. Microapp is a catalogue of small static pages, each one wrapping a single function. Treating it like an app meant paying the app overhead on every page.
The fix was the architectural choice that produces every other one in this chapter. Astro Static Site Generation. Every page exports prerender = true; the build produces a directory of static HTML files; the Cloudflare CDN serves them from the edge with no Worker invocation. React widgets become islands — hydrated only on the pages that need them, only after the static HTML has painted. The home page now ships a one-kilobyte JavaScript bundle.
Static HTML at the edge. JavaScript only on the islands that need it.
The PageSpeed score moved to 95. The Time to Interactive dropped to under a second. The hosting cost dropped to roughly $5 a month — Cloudflare Pages serves static files free up to the kind of volume that would already be telling us something interesting about demand. The shape suits the product: the user came for a calculator, the calculator should appear instantly, and any JavaScript should only run if the calculator needs it.
The transferable why: the rendering strategy is the architectural choice with the biggest downstream consequences. App-shape renders treat every page as dynamic by default; site-shape renders treat every page as static by default. Most catalog-shaped products are site-shape. Picking the wrong shape on day one costs hundreds of milliseconds on every page load forever — and the cost compounds with every interaction the user makes before the JS arrives.
Locked 2026-05-03 · Astro SSG · output: "static" · Cloudflare Pages
The second decision was the one that ended a class of merge conflicts I'd lived with for months.
The catalog of tools used to live in a hand-edited TypeScript file — src/lib/tools-data.ts. Every Bob PR added a line to it. Every parallel PR produced a merge conflict on that file because two PRs both added entries near the bottom. Bob was supposed to alphabetize the array, didn't always remember, and the conflicts compounded. By the day I sat down to fix this, every parallel PR ran a 30% chance of needing a manual merge resolution. The bottleneck wasn't the work the agents were doing; it was the file the work converged into.
The change that survived was moving the canonical catalog into Supabase. The tool_metadata table is now the source of truth: slug, label, description, category, keywords, isActive, plus the long-form SEO copy. Bob's PRs stop touching the TypeScript catalog; instead each PR seeds a Supabase row. The TypeScript file becomes tools-data.generated.ts — a build-time mirror produced by pnpm gen:tools, run automatically in predev and prebuild hooks. The mirror is committed so a fresh clone has it for IntelliSense and astro check; the canonical source is the database.
The catalog lives in Supabase. The TypeScript is a mirror generated at build.
The merge conflicts vanished overnight. Two Bob PRs that ship the same week each seed their own Supabase row, never touch the same file, never collide. The TypeScript mirror is regenerated locally on each branch — if the local generation fails for any reason, the build degrades gracefully and uses the previous artifact rather than failing the whole pipeline. Supabase becomes a hard dependency at build time, which is a real cost — but the alternative cost was a manual merge resolution on every parallel PR, paid forever.
The transferable why: source of truth is a choice about where conflicts converge. Hand-edited files in a repo converge conflicts on humans; databases converge them inside transactions. When work is going to come from many sources — many agents, many contributors, many branches — picking the right convergence point matters more than picking the right schema. The schema can evolve; the convergence point is harder to change.
Locked 2026-05-03 · Supabase tool_metadata · TS mirror via pnpm gen:tools
The third decision was about how a single microapp is shaped — and the answer turned out to need three tiers, not one.
The first version assumed every tool was a custom React widget. Every microapp got its own .tsx file, its own UI choices, its own state shape. The system was elegant and inefficient. Sixty percent of the catalog is calculators and converters — tools whose UI is essentially a form with two-or-three inputs, a button, and a result. Writing a custom widget for each one was a waste of agent attention.
The shape that survived has three tiers. Tier 1 is the engine-only microapp: a JSON config in tool-registry.ts describes the inputs, outputs, and conversion logic. The <ToolEngine> component renders the whole UI from the config. Adding a Tier 1 tool is a fifty-line config change. Forty-plus tools live at Tier 1 today; each one was the same shape before the tier existed and each one is now the same config-shape afterward.
Tier 2 is the custom-widget microapp: a .tsx file in src/components/tools/, hand-written when the tool needs UI that the engine can't render — a clicker counter with persistent state, an image cropper with mouse handling, a stats calculator with a multi-row table. The widget gets auto-discovered by ToolIsland via a Vite glob; no manual registry. Roughly half the catalog lives here.
Tier 3 is the long-form content microapp: an Astro page with editorial content, embedded interactive elements, multiple regions. The Brand page, the membership page, this book. These are rare and intentional — they exist when the tool's "use" includes reading and the page itself is the experience.
Tier 1. Tier 2. Tier 3. Each new microapp picks one.
The tier system is the abstraction that lets the catalog scale without each new tool relitigating its UI shape. Bob asks Kai which tier the new tool belongs in; Kai's spec picks one; Bob builds against that tier's conventions. The cognitive load on every other agent in the pipeline drops by exactly the size of the decision the tier system absorbed.
The transferable why: tier systems collapse a continuous decision space into a small set of named choices. Without tiers, every new instance asks the same questions from scratch — what UI, what state, what conventions. With tiers, the question becomes "which tier?" and the rest of the decisions are inherited. The cost is the discipline of naming the tiers well enough that the inheritance is real. The benefit is the rest of the team stops paying the same decision twice.
Locked 2026-05-08 · Tier 1 / 2 / 3 microapp shapes · auto-discovery via Vite glob
The fourth decision is the one that keeps the build resilient when the dependencies aren't.
Switching to Supabase as the canonical catalog introduced a build-time dependency on Supabase being reachable. If the build runs on a contributor's laptop with no internet, or on a CI runner with a temporarily down Supabase, or on the day Supabase's status page lights up red — the catalog generation can't reach the database, and the natural failure mode is a build error that blocks shipping.
The version that survives degrades gracefully. pnpm gen:tools tries to reach Supabase; if it succeeds, it writes a fresh tools-data.generated.ts. If it fails — network down, credentials missing, Supabase unreachable — the generator logs the failure and leaves the existing artifact in place. The build proceeds with the last-known-good catalog rather than failing outright. The same pattern applies to translations: getAllToolTranslations() returns an empty object on failure, so locale pages render in English rather than failing the entire build.
Graceful degradation. The build never fails for a reason it could have lived through.
The cost is the risk of shipping a slightly stale catalog when a build runs during an outage. The benefit is that the build pipeline never goes down because Supabase did. The trade reads correctly given the asymmetry — stale-catalog-by-an-hour is a recoverable problem; failed-deploy-during-an-outage is a problem that compounds with whatever else needed to ship that hour.
The transferable why: hard dependencies between independent systems should fail soft, not hard, at the boundary. The right question to ask of every external call in a build pipeline is "what does this do if the other side is down?" The honest answer is usually "crash the build" — and the honest answer is usually wrong. Build pipelines should be more reliable than the systems they read from, because the cost of downtime in a pipeline is the cost of downtime in every system downstream of it.
Locked 2026-05-03 · graceful degradation · build keeps last-known-good artifact
The last decision was about where to run the production site, and the answer was the boring one.
The candidates were the obvious set — Vercel, Netlify, Cloudflare Pages, AWS via Amplify, a self-hosted nginx on a $5 VPS. Each one would have served the static HTML acceptably. The criteria that mattered were three: cost at the scale we're at today, cost at the scale we project to in two years, and the proximity of the runtime to the rest of the agent stack.
Cloudflare Pages won three for three. Free tier covers the current scale with substantial headroom. Cost at 10x scale is still under $20/month. The rest of the agent stack already runs on Cloudflare Workers — microapp-ai, microapp-mcp, agent-os-router, agent-os-broadcaster — and the operational benefit of running the website on the same platform compounds: one dashboard, one billing line, one DNS provider, one set of edge regions. The integration cost of running Pages alongside Workers is zero; the integration cost of running anywhere else would be paid in glue code for the rest of the company's life.
Cloudflare Pages. Same platform as the rest of the agent stack.
The integration with Astro is also unusually smooth — @astrojs/cloudflare is a maintained adapter, wrangler works the same way locally as in production, the preview-on-PR feature comes free. None of these are deal-breakers individually; together they reduce the operational friction by an order of magnitude versus the alternatives.
The transferable why: infrastructure choices benefit from compounding around a small set of vendors. Each additional vendor in the stack adds an integration surface, a billing line, a dashboard to monitor, an outage to track. Picking a vendor that already runs adjacent parts of the stack pays a small premium today and a large compounding return for the rest of the company's life. The alternative — best-of-breed at every layer — looks defensible on paper and is exhausting to operate.
Locked 2026-05-03 · Cloudflare Pages · same platform as the agent stack
That's the system design. Static at the edge, Supabase as the catalog, three tiers per microapp, graceful degradation across boundaries, Cloudflare as the substrate. Every chapter downstream of this one — the engines, the agents, the autonomous loop — runs against the architecture these five decisions produced.