Accepted

ADR 0001 — Hono as the HTTP router

One Worker, one Hono app, sub-routers for the admin API, headless API, and public render

Cusp is one Worker hosting four logical handlers: the admin API, the headless REST API, the public render path, and the Workers Static Assets binding for the admin SPA bundle. They share routing, auth middleware, request parsing, and observability. The decision is which router and middleware framework lives inside the Worker.

Decision

Use Hono as the router and middleware framework. The Worker entry at src/worker/index.ts builds one Hono app and mounts sub-routers under prefixes. Anything that matches no route falls through to the public render handler, and then to the static-assets binding for real static files.

const app = new Hono<AuthHonoEnv>();

app.route("/api/admin", adminApi);  // cookie-authenticated admin API
app.route("/api/v1", headlessApi);  // bearer-token headless REST

// Unmatched GETs run the public render path, with ASSETS as the
// final fallback for static files shipped with the build.
app.get("*", publicRenderHandler);

Route handlers receive a typed context with the Env shape populated from the generated worker configuration types, so a handler knows its bindings without manual casts. The admin surface uses a cookie-auth middleware stack and the headless surface uses a bearer-token stack, but both run on the same dispatcher and share one auth code path. The per-document edit session is a WebSocket upgrade served inside the admin API (GET /api/admin/documents/:id/session), which authenticates the request and forwards it to the DocumentSession Durable Object.

Why Hono

  • Runs natively on the Workers runtime with no Node-specific assumptions and no large polyfill surface.
  • Small footprint — single-digit kilobytes after tree-shaking, which protects the Worker bundle budget.
  • Clean middleware composition for auth, per-token rate limiting, request-ID logging, and cache-key derivation.
  • Fits the static-assets fall-through pattern: dynamic routes are handled, everything unmatched is served by the ASSETS binding.
  • TypeScript ergonomics good enough that route handlers know the Env shape without per-call casts.

Consequences

The same router serves admin, headless, and public flows, which keeps the four handlers from drifting into independent codebases. Handlers stay thin and call into framework-agnostic business logic under src/shared/, so the lock-in to Hono's context and middleware shapes is contained. Astro renders pages on the public path; Hono routes requests. They cooperate rather than compete — Astro's render is invoked from inside a Hono route, neither replaces the other.

Considered and rejected

  • A bare fetch handler — smallest bundle, but forces every contributor to invent their own routing and middleware, which fragments the codebase. The implicit framework would end up larger and worse than Hono.
  • itty-router — smaller, but lacks the middleware ergonomics and inference the auth and rate-limit work need.
  • Express-shaped frameworks — built for Node, large polyfill surface, poor fit for the Workers cold-start budget.
  • Astro's request handler as the router — Astro renders pages; the admin and headless APIs are not Astro-shaped, so routing them through Astro would couple unrelated concerns.
API design and surfaces