From b8f0abdb0d158298f7bc834ee4250b03d34bd613 Mon Sep 17 00:00:00 2001 From: implementer Date: Mon, 31 Aug 2026 00:34:08 +0000 Subject: [PATCH] docs(adr): record Fastify 5 HTTP runtime decision as ADR-004 --- docs/adr/ADR-004-fastify-5-http-runtime.md | 136 +++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 docs/adr/ADR-004-fastify-5-http-runtime.md diff --git a/docs/adr/ADR-004-fastify-5-http-runtime.md b/docs/adr/ADR-004-fastify-5-http-runtime.md new file mode 100644 index 0000000..be57014 --- /dev/null +++ b/docs/adr/ADR-004-fastify-5-http-runtime.md @@ -0,0 +1,136 @@ +# 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. -- 2.54.0