Why Fastify for the Dashboard API
Status: Decided. Running in production as chris-os-dashboard-api.
Context
Section titled “Context”The dashboard needs a backend API that serves personal data from PostgreSQL to the React frontend. Requirements:
- REST endpoints for health records, messages, calendar, Peak State scores, documents, dispatch panel, container status
- Server-Sent Events (SSE) for real-time dashboard updates
- Write-back endpoints for reviews, notifications, and push subscriptions
- Authentication via Authelia’s
Remote-Userheader (set by Caddy’sforward_auth) - Rate limiting keyed on
Remote-User - Docker socket proxy integration for container status widget
- Home Assistant REST API proxy
- Claude session log access (read-only mount)
The API runs behind Nginx (which serves the compiled React SPA and proxies /api/* to the Fastify server). Both are custom-built containers deployed on Caroline.
Decision
Section titled “Decision”Fastify with TypeScript.
The API uses Fastify’s plugin system for route organization, Zod for schema validation, and @fastify/rate-limit for per-user rate limiting. PostgreSQL connections use pg-pool with separate pools for read-only queries and write operations. SSE is implemented using Fastify’s reply piping combined with PostgreSQL LISTEN/NOTIFY.
What Was Rejected and Why
Section titled “What Was Rejected and Why”Next.js API routes: The original plan considered a unified Next.js application serving both the React frontend and API routes from a single Next.js server. This was rejected for several reasons.
Next.js API routes are designed around the serverless function model: short-lived handlers, no persistent connections, no background work. A Postgres connection pool that stays alive for LISTEN/NOTIFY SSE streaming does not fit this model cleanly. Next.js 13+ App Router further complicates things by mixing server components, client components, route handlers, and server actions in ways that add conceptual overhead for what is fundamentally a REST API serving a SPA.
The framework itself — Next.js’s webpack configuration, image optimization pipeline, ISR, file-system routing conventions — is overhead for an internal tool with one user. Fastify is a web framework. Next.js is a full-stack framework. The right tool for a REST API is the former.
Express: Express was considered and discarded in favor of Fastify for two reasons. First, Fastify’s benchmark numbers are consistently 2-3x better throughput than Express on equivalent endpoints. For a Pi 5 running 37 containers, every avoided millisecond of framework overhead is real. Second, Fastify’s plugin system with TypeScript support and built-in schema validation (via the schema option on route definitions) provides guardrails that Express’s middleware model does not.
tRPC: tRPC provides end-to-end type safety between server and client without a separate schema definition step. The tradeoff is that it requires the client to use a tRPC adapter, which conflicts with the goal of keeping the frontend independent of the backend transport layer. The React SPA uses standard fetch calls to REST endpoints. tRPC’s advantages are most meaningful in teams where the backend and frontend are maintained by the same TypeScript codebase and the API surface changes frequently. For a stable personal API, plain REST is simpler.
Hono: Hono was evaluated briefly. It is faster than Fastify on some benchmarks and has excellent TypeScript support. The ecosystem around Fastify (plugins for rate limiting, CORS, cookie handling, multipart, WebSocket) is more mature. Fastify’s explicit plugin lifecycle (register → decorate → hook → route) maps well to the initialization order required by the database pool setup and the Authelia auth checks.
Consequences
Section titled “Consequences”What works well:
- The plugin architecture keeps routes organized. Each dashboard section (health, messages, calendar, dispatch) lives in its own Fastify plugin registered at a path prefix.
- Schema validation on request bodies catches malformed inputs before they reach the database layer. The Zod integration provides TypeScript-inferred types from the same schema definitions used at runtime.
- SSE works cleanly. The LISTEN/NOTIFY pipeline from Postgres through Fastify to the browser client is stable and low-overhead. The same architecture handles the dispatch panel’s real-time activity feed, push notification state updates, and morning briefing streaming.
- Rate limiting keyed on
Remote-User(not IP) correctly handles clients behind the Caddy reverse proxy without needingtrustProxy: trueIP parsing.
Tradeoffs accepted:
- No full-stack framework benefits. Shared types between frontend and backend require manual coordination or a shared types package. Currently the types are duplicated; this is a known paper cut.
- Fastify’s error handling model (error handlers registered as plugins, not middleware) has a learning curve. Unhandled errors in async route handlers behave differently than in Express.
- The
dashboard-apicontainer is a custom build. Every code change requires a container rebuild and redeploy (handled by the CI pipeline). There is no hot-reload in production.
Authentication Note
Section titled “Authentication Note”The Fastify API trusts user identity headers set by Authelia via Caddy’s forward_auth. These headers are only reachable after Authelia authentication. Any new API route that is added to both the Fastify server and the Caddy configuration must also be added to Authelia’s bypass list if it is intended to be accessible without SSO (for example, webhook endpoints that receive inbound data).
Failing to add a new route to both Fastify’s machine-to-machine path list and the Caddy handle blocks will result in the route being either unprotected or unreachable. This is a documented gotcha.