Skip to content

004 — tRPC API (not GraphQL, server functions, or subscriptions)

Context

One first-party web app talks to one Worker. Both sides are TypeScript. Procedures are already namespaced (auth.*, domains.*) with Zod inputs and stable appCode errors (API reference).

Decision

Use tRPC (fetch adapter) on the Worker and @trpc/client + @trpc/react-query on the web. Send cookies with credentials: 'include'. Shared error codes live in @send-again/shared.

Why not GraphQL

  • GraphQL shines with many clients, flexible field selection, and a published schema. This SPA almost always needs full DomainDetail / list shapes — not field shopping.
  • Would add SDL (or code-first GraphQL), resolvers, and often codegen in addition to Zod already used for inputs — duplicate contract surface for little gain.
  • Subscriptions/realtime are unused; cron stays on the Worker, not in the API layer.

Why not server functions (Next / Remix / TanStack Start-style)

  • Server functions couple UI and backend into one framework deploy. This prototype deliberately splits Vite SPA + Worker (ADR 005, ADR 001) so cron, D1, and API share the Worker without a full-stack meta-framework.
  • Colocated server actions blur the domain service boundary the challenge is meant to showcase; tRPC procedures stay thin adapters over services/repos.
  • Server functions often assume Node or framework-specific runtimes; Workers + Wrangler already define the server target.

Why not hand-rolled REST + OpenAPI

  • Would re-declare the same input/output types the SPA needs, or maintain OpenAPI as a third artifact. Fine for public/multi-language APIs — not needed for one TypeScript client.

Realtime: client polling, not tRPC subscriptions

  • DNS verification is slow (server poll interval / cron ~5 min). WebSocket push does not speed up DNS and adds Workers realtime complexity (hibernation/Durable Objects, reconnect, cookie auth on long-lived connections).
  • Verification/claim UI should use short client polling of existing domains.verify / domains.get while the screen is open (e.g. every 15–30s or on focus), stopping on terminal status or navigation. The server still enforces DNS_POLL_INTERVAL_MS, so extra client ticks mostly return cached state.
  • tRPC subscriptions are deferred; revisit only for true multi-user live collaboration or instant cross-tab alerts beyond email + next fetch.

Consequences

  • Procedure renames and breaking input/output changes show up at compile time in the SPA.
  • Docs still describe the HTTP shape for humans.
  • A public third-party API, multi-language clients, or highly flexible querying would push toward REST/OpenAPI or GraphQL later — out of scope for this prototype.