Files
PersonalBlog/docs/adr/ADR-004-fastify-5-http-runtime.md
T
implementer b8f0abdb0d
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (pull_request) Successful in 1m13s
CI / Stage 4 — Unit tests (E00-S05-T01) (pull_request) Successful in 1m40s
CI / Stage 5 — Architecture tests (E00-S05-T01) (pull_request) Successful in 3m11s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (pull_request) Successful in 1m8s
CI / Stage 2 — Typecheck (E00-S05-T01) (pull_request) Successful in 1m12s
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (pull_request) Successful in 44s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (pull_request) Successful in 44s
docs(adr): record Fastify 5 HTTP runtime decision as ADR-004
2026-08-31 00:34:08 +00:00

7.5 KiB

ADR-004: Fastify 5 HTTP runtime

  • Status: Accepted
  • Date: 2026-08-31
  • Deciders: platform stream
  • References: ADR index (section 70), Technology stack wiki (sections 5.2, 6, 7, 8), Architecture wiki (sections 1, 19, 42), ADR-001, ADR-002

Context

EPPP is a modular monolith (ADR-001) whose single application image contains the public server, admin API, rendering pipeline, extension runtime and core services. Every inbound request — public pages, the admin API and the health/readiness endpoints — enters the platform through one HTTP runtime, so the runtime choice is a platform-wide, hard-to-reverse decision with security, tooling and operational reach.

The public rendering pipeline is already documented as starting at the HTTP layer — Fastify route → SiteResolver → VisitorPreferenceResolver → ContentService → PageComposition + BlockRegistry → ThemeResolver → React DOM server renderer → HTML (Architecture wiki §19) — and the v0.1 public surface (§42) exposes GET /, GET /posts/:slug, GET /assets/*, GET /media/*, GET /health/live, GET /health/ready and GET /admin/*, all served by the HTTP runtime.

The choice is already recorded higher up: ADR-001 fixes the runtime as "Node.js 24 LTS with Fastify 5" and ADR-002 records the Node line. The technology stack wiki pins Fastify 5.12.1 in the golden compatibility tuple (§7) as support class B — a documented support policy with no fixed multi-year EOL date (§5.2) — and lists the approved Fastify lifecycle plugins: @fastify/cookie 11.1.2, helmet 13.1.1, @fastify/rate-limit 11.2.0, @fastify/static 10.1.3, @fastify/csrf-protection 8.0.1, @fastify/swagger 9.8.1, with @fastify/multipart 10.1.1 behind a CI gate (§5.2).

The workspace currently boots a minimal node:http server for the health endpoint (E00-S02-T03) — explicitly documented as a bootstrap until "the Fastify 5 application shell (and the real HTTP API) lands in a later story" — so this ADR formalises the committed runtime decision ahead of that shell.

Decision

EPPP serves all inbound HTTP on the Fastify 5 HTTP runtime, exact-pinned to Fastify 5.12.1 in the golden compatibility tuple (Technology stack §7). One Fastify 5 application hosts the public routes, the admin API under /api/admin/v1/* and the health/readiness endpoints; the temporary node:http bootstrap is replaced when the Fastify 5 application shell lands in a later story.

Fastify's plugin encapsulation matches the platform's module-boundary discipline (ADR-001): the approved plugin set is the Fastify lifecycle list in the technology stack (§5.2) — cookie, helmet, rate-limit, static, csrf-protection, swagger, with multipart gated in CI before enabling — and Fastify's native Pino 10.3.1 logger is the platform logger (the existing secret-redaction requirement, E00-S04-T03, carries over).

Fastify is support class B: it has no fixed upstream EOL, and an upgrade to Fastify 6 is a deliberate, ADR-recorded major change gated on a stable v6, all required plugins compatible, the full suite green, and extension contracts stable/migrated (§6). The decision text — Fastify 5 HTTP runtime — matches the ADR index entry (ADR-004, section 70).

Alternatives

  • Express (4.x/5.x) — rejected: the middleware-chain model has no first-class request/response schema validation or serialization, which conflicts with the JSON Schema + TypeBox + Ajv validation decision (ADR-011); plugin encapsulation and lifecycle hooks are weaker than Fastify's, and the community middleware surface is less uniform to pin.
  • Hono — rejected: it is oriented toward edge/serverless runtimes, which conflicts with the long-lived process model, background jobs and connection-backed PostgreSQL access chosen in ADR-001; the golden tuple and extension contracts were bootstrapped on Fastify.
  • Koa — rejected: the minimal core requires assembling routing, body parsing, validation, security headers and logging by hand, adding glue code with no schema-based validation story.
  • Bare node:http — rejected for the real API: it is only the temporary health bootstrap (E00-S02-T03); it provides no routing, plugin lifecycle, validation or the ecosystem the v0.1 public surface (§42) needs.
  • Fastify 5 — chosen: schema-based validation and serialization aligns with ADR-011, plugin encapsulation matches the module boundaries (ADR-001), Pino logging is native, TypeScript support is first-class, and the lifecycle plugin set is already pinned in the stack (§5.2).

Consequences

  • Positive: schema-based request/response validation and serialization aligns with the ADR-011 decision (JSON Schema + TypeBox + Ajv); plugin encapsulation gives each plugin an isolated scope, matching the module-boundary discipline of ADR-001; the runtime is already the documented choice in ADR-001 and the golden tuple, so no rework is needed when the application shell lands; native Pino 10.3.1 logging integrates with the existing redaction requirement (E00-S04-T03); the pinned lifecycle plugin set covers the v0.1 needs — cookies/opaque DB-backed sessions (ADR-019), security headers, rate limiting, static assets/media and swagger docs.
  • Negative: Fastify 5 is class B — no fixed upstream EOL date, so the support horizon is policy-defined rather than date-defined; the exact pin (5.12.1) must move deliberately through the dependency lanes; the application shell does not exist yet, so this ADR commits a decision whose implementation lands in a later story (the node:http bootstrap remains until then).
  • Neutral: Fastify is an in-process library, so a future module extraction (ADR-001) does not change the shared HTTP runtime unless that module needs its own runtime; plugin majors move through the update lanes like any class B dependency.

Operational impact

  • One Fastify 5 process serves the public routes, admin API and health/readiness endpoints; it binds the validated HOST/PORT from the config adapter (E00-S04-T04).
  • Health/readiness remain part of the v0.1 surface (GET /health/live, GET /health/ready, Architecture wiki §42) with readiness gated on migration completion (E00-S03-T06).
  • All log output continues through the redacting-logger pattern (E00-S04-T03); Fastify's native Pino logger is configured with the same redaction.
  • The security posture comes from the approved plugin set: helmet (headers), csrf-protection (state-changing requests), rate-limit (abuse), cookie (opaque DB-backed admin sessions, ADR-019), static (assets/media) and swagger (API docs).
  • Deploy/rollback is unchanged: the HTTP runtime lives inside the single application image (ADR-001), so rollback is redeploying the previous image.
  • Fastify patch/minor upgrades go through the weekly dependency sweep with full CI; a Fastify 6 major is a programme item (ADR + compatibility + full suite + plugin migration, §6/§8).

Revisit trigger

  • Revisit when Fastify 6 is stable, all required lifecycle plugins are compatible, the full suite is green and the extension contracts are stable/migrated — per the LTS strategy (§6) the move to v6 is a new ADR, not a patch.
  • Revisit if the Fastify 5 support policy changes (class B has no fixed EOL date), or if a required plugin forces a Fastify major earlier than planned.
  • Revisit if the Fastify 5 application shell, when it lands, cannot satisfy the v0.1 public surface (§42) or the v1.1 boundary contracts (ADR-027 to ADR-032) — for example, a hard requirement Fastify 5 cannot meet.