Skip to content

Worker API & Clerk Auth

The HTTP API is a Cloudflare Worker built with Hono and @hono/zod-openapi, assembled by createApp() in src/api/index.ts and mounted under /api.

The factory wires a fixed middleware chain ahead of the per-feature route registrars:

flowchart LR
  W["src/worker.ts<br/>Worker entry"] --> APP["createApp()<br/>src/api/index.ts"]
  APP --> SEC["secureHeaders<br/>(API CSP)"]
  SEC --> OC["originCheck<br/>origin-check.ts"]
  OC --> ERR["onError + problem()<br/>error.ts (RFC 9457)"]
  ERR --> CLERK["clerkAuth<br/>clerk-auth.ts"]
  CLERK --> CK{{"Clerk<br/>session verify"}}
  ERR --> ROUTES["register*Routes(app)<br/>lists / tasks / steps / ..."]
  APP --> DOC["doc('/v1/openapi.json')<br/>OpenAPI 3.1"]

createApp() returns an OpenAPIHono instance with .basePath('/api'). A defaultHook turns Zod validation failures into a uniform RFC 9457 422 validation_error (ADR-002) rather than Hono’s default 400. Routes are registered per feature by register*Routes(app) functions (registerListsRoutes, registerGroupsRoutes, registerTasksRoutes, registerStepsRoutes, registerMyDayRoutes, registerListMembersRoutes, registerSyncRoutes, registerMeRoute) and are versioned under /v1 (ADR-001).

  • secureHeaders — applied to every response with an enforcing default-src 'none' CSP (API responses are JSON/problem+json with no subresources). The WebSocket upgrade (GET with Upgrade: websocket) is exempted, because its immutable 101 response cannot carry headers (src/api/index.ts).
  • originCheck — cross-origin guard (src/api/middleware/origin-check.ts).
  • error middlewareonError + problem() produce the RFC 9457 envelope (src/api/middleware/error.ts, ADR-002).
  • Clerk — authentication is the single boundary (constitution §7.12); the user identity is derived from the verified session, never from client input. Auth UI uses Clerk prebuilt components (ADR-010).

A request that fails validation short-circuits at the defaultHook; one that fails auth short-circuits in clerkAuth; only a verified request reaches the route handler:

sequenceDiagram
  participant Client
  participant App as "createApp() (OpenAPIHono)"
  participant Clerk as "clerkAuth"
  participant Backend as "Clerk backend"
  participant Route as "route handler"
  Client->>App: POST /api/v1/...
  App->>App: secureHeaders, originCheck
  App->>Clerk: clerkAuth
  Clerk->>Backend: authenticateRequest(req.raw)
  alt no auth.userId
    Backend-->>Clerk: unauthenticated
    Clerk-->>Client: 401 unauthorized (RFC 9457)
  else verified
    Backend-->>Clerk: auth.userId
    Clerk->>Route: c.set('auth'), next()
    Route-->>Client: 2xx JSON (or 422 validation_error)
  end

Routes are declared with createRoute({ ... }) and Zod schemas, so the OpenAPI document is generated from the code (ADR-003) — it is the single source of truth shared by all clients and is rendered as this site’s API Reference. scripts/build-openapi.mjs emits it to openapi.json at build time.

Related: DB Schema & Notify-Affected (what the routes write and who they notify), SyncInbox Durable Object (the upgrade route’s target).