Accepted
ADR 0004 — Admin SPA in React, built by Astro
The admin is a React single-page app, built by Astro and served from Workers Static Assets — never part of the Worker bundle
The admin UI is the product. It carries the block editor, the content-type builder, the version-history viewer, and the time-travel UI — a substantial interactive surface, and the primary touch point for a non-technical audience. It must not weigh on the Worker bundle.
Decision
Build the admin SPA with Astro as the build orchestrator and React for the interactive surfaces. The output goes to a separate static bundle served by the ASSETS binding. The Worker imports nothing from src/admin/.
- Astro renders the SPA shell (login page, setup-wizard skeleton, admin entry HTML) at build time and embeds the React islands that hydrate inside it.
- React is the interactive framework — every screen with state is a React island. The block editor uses TipTap inside text blocks; the surrounding block list is custom React with drag-and-drop.
- Routing is client-side. The Worker rewrites every /admin/* request to /admin and serves the shell from ASSETS; the SPA reads the route off the URL and renders the matching screen.
- A single shared client (src/admin/src/lib/api-client.ts) talks to /api/admin/* over JSON — one data layer, not ad-hoc fetches scattered through components.
Why React on Astro
The editor-component ecosystem is React-shaped. TipTap has a first-class React integration; the block editors of reference are all React. Choosing a smaller-bundle framework would save kilobytes but lose access to those libraries and turn the marquee block editor into a from-scratch build. Reusing Astro means one frontend toolchain for the admin build. [Amendment (ADR 0011): Astro is no longer the public render framework — the public path renders through plain TypeScript template functions, since .astro files do not survive the Worker bundling pipeline. Astro's role narrowed to building the admin SPA; the single-toolchain rationale applies to the admin only.]
Consequences
The boundary between Worker and admin is enforced by being literally separate bundles: an admin component cannot import the Worker's env by accident. The pre-rendered shell shows the admin chrome before the React bundle parses, which is the right ergonomic for a non-technical user. The cost is that React is not small — but it is paid entirely by the static-assets bundle, never by the Worker. The Worker's own bundle budget (3 MB compressed on the free plan) is enforced in CI by pnpm build:size against dist/worker, which the admin code never reaches.
Considered and rejected
- Solid or Svelte on Astro — smaller bundles, but a far smaller pool of editor-component libraries; the block editor would be largely from scratch.
- Vue on Astro — acceptable, but a smaller ecosystem for the specific editor libraries needed, with no positive reason to prefer it over React.
- Server-side-rendered admin via the Worker — forces a round trip for every navigation; the block editor is fundamentally a client-side app and suffers under SSR.
- React without Astro (a hand-rolled Vite build) — loses the pre-rendered shell and the shared toolchain for the public render path, and costs more to maintain.
Related: ADR 0001 — Hono router covers how the one Worker mounts the admin API, the headless API, and the public render handler alongside the ASSETS fall-through.