ADR-0001: Adopt Hono as the HTTP framework on every non-Astro Worker
ADR-0001: Adopt Hono as the HTTP framework on every non-Astro Worker
Section titled “ADR-0001: Adopt Hono as the HTTP framework on every non-Astro Worker”- Status: Accepted (2026-05-24)
- Deciders: @ArneFfm
- Master design impact: Adds D40. No existing D-row (D1–D39) is contradicted.
- Amended 2026-06-07 (master D42): the admin frontend is Hono, not an “eventual Astro layer.”
apps/adminrenders its HTML server-side via Hono and consumes the design system through the renderer-agnostic CVA*.variants.tscontract +@nowyourlink/design-tokens— not the.astroprimitives. The Decision below is corrected accordingly.
Context
Section titled “Context”Today every non-Astro Worker (apps/api, apps/admin, apps/webhook, apps/queue, apps/cron) hand-rolls its dispatch with new URL(request.url) + if (url.pathname === "...") chains. Each Worker also duplicates an errorBoundary(request, env, handler) function — ~50 lines of copy-pasted request-id + problem+json plumbing per worker. This is brittle (drift between workers is silent), uncovered (the inline errorBoundary in apps/admin/src/index.ts is dead code — the handler never wraps in it), and friction-heavy for sub-system specs 02, 03, 05, 08, 09 which add ~30 HTTP endpoints over the next phase.
Master design D1–D39 do not pick an HTTP framework. The slot is open.
Decision
Section titled “Decision”Use Hono as the canonical HTTP framework on every non-Astro Worker. apps/web stays Astro per D17; apps/admin — including its frontend — is a Hono worker (master D42), consuming the design system via the CVA variant contract + design-tokens rather than the .astro primitives; docs-site/ stays Starlight per D18.
The Hono app lives in src/app.ts per Worker. src/index.ts keeps the Durable Object exports (SessionDO, UserRead) and wraps app.fetch in Sentry.withSentry to preserve the master §4.6 instrumentation contract.
Alternatives considered
Section titled “Alternatives considered”| Option | Why rejected |
|---|---|
Status quo (hand-rolled URL + if) | Five copies of the same 50-line errorBoundary; drift between workers is undetected; admin’s copy is already dead code. |
itty-router | Smaller bundle but no first-class middleware after-pattern (await next() then mutate response) — request-id echo would require manual response cloning. No onError contract. |
@cloudflare/workers-routing | No app.request()/app.fetch() helper for in-memory tests; less mature TypeScript inference for Bindings. |
Hono + hono/jwt + hono/cors + @hono/zod-validator | Out of scope — adopt only hono core in this PR. Helpers land per-spec when their owning issue does (e.g. spec 08 §6’s CORS is the natural moment to introduce hono/cors). |
What stays unchanged
Section titled “What stays unchanged”packages/errors—HttpError,toProblemJson,InvariantErrorare the byte-identical source of the RFC 7807 envelope. Honoapp.onErrorcallstoProblemJson(err); it does not use Hono’sHTTPException(which is not aware ofContext-set headers per the Hono docs —HTTPException.getResponse()would drop theX-Request-Idset in middleware).packages/logging—createLogger,sentryTransport, theLogLineschema and field order. Hono middlewarec.set("logger", ...)exposes the same per-requestLoggerto handlers viac.get("logger").@sentry/cloudflareSentry.withSentry(opts, handler)— wraps the module fetch verbatim. The Hono app is passed in as{ fetch: app.fetch }.- Route paths, status codes, response bodies, auth/session contracts, DO and Queue bindings,
wrangler.jsonc.
Scope — workers in/out
Section titled “Scope — workers in/out”- Adopted:
apps/api,apps/admin,apps/webhook,apps/queue,apps/cron. apps/queueandapps/croneach currently have a fetch handler (queue’s returns a “use the binding” probe; cron’s returns “Hello from cron”). Both adopt Hono.- Not adopted in this PR:
apps/web(Astro / D17),apps/ws(out of scope per the migration brief — left untouched), Durable Objects (their ownfetch()contract is owned by Workers’ runtime, not by a router), Cloudflare Workflows (step.doorchestration is not HTTP).
Per-worker invariants
Section titled “Per-worker invariants”apps/api,apps/webhook,apps/queue,apps/cron: Hono app with a request-id middleware that resolves the inboundX-Request-Id(regex/^[A-Za-z0-9._-]{1,128}$/), mints a UUID otherwise, sets it on the context, and echoes it on the outbound headers afterawait next()viac.header(REQUEST_ID_HEADER, requestId).app.onErrormapsHttpError → toProblemJson(err)(orInvariantErrorfallback) and stamps the request-id.apps/admin: Hono route table with no request-id middleware and noapp.onError. Its current handler returns plainResponseobjects (Forbidden, JSON, HTML) without a problem+json envelope and without echoingX-Request-Id— the migration preserves that. Any future symmetricerrorBoundaryrequirement is tracked as a follow-up.
Consequences
Section titled “Consequences”Positive:
- One source of truth for request-id + problem+json envelope on api/webhook/queue/cron (the Hono middleware +
app.onError). app.fetch(request, env, ctx)enables fast in-process integration tests without spinning upwrangler dev— see spec 13 §3.1’sSELF.fetchis still available for end-to-end coverage, but unit-style route round-trips now go throughapp.fetch()directly.- New endpoints in spec 02/03/05/08/09 are a single
.post('/path', handler)line each.
Negative:
- One more dependency (Hono ^4, ~30 KB unminified, ~5 KB gzipped; well under Workers’ 1 MiB script limit).
- Each worker’s existing handler unit tests stay valid;
app.fetch()round-trips are added on top, growing the test surface ~3x for the small workers.
Follow-ups (NOT in this PR)
Section titled “Follow-ups (NOT in this PR)”- Migrate
apps/wsto Hono when the WebSocket upgrade routing pattern is reviewed alongside the spec 06 Durable Object work. - Adopt
hono/corswhen spec 08 §6’sPOST /convingest endpoint lands (issue not yet open). - Adopt
@hono/zod-validatorwhen a sub-system needs schema-validated request bodies — current handlers do their own narrow type guards inline (seeapps/api/src/auth-routes.ts:144-156). - Extract the request-id middleware + Sentry-logger wiring into
packages/middlewareonce at least three workers are stable on the current pattern — deferring keeps this PR’s invariant (“shape stays the same”) intact.