docs(adr): record React SSR for public rendering decision as ADR-007
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (pull_request) Successful in 1m14s
CI / Stage 4 — Unit tests (E00-S05-T01) (pull_request) Successful in 1m37s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (pull_request) Successful in 1m3s
CI / Stage 5 — Architecture tests (E00-S05-T01) (pull_request) Successful in 3m14s
CI / Stage 2 — Typecheck (E00-S05-T01) (pull_request) Successful in 1m20s
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (pull_request) Successful in 51s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (pull_request) Successful in 45s

This commit is contained in:
implementer
2026-08-31 00:51:41 +00:00
parent 2a229bbf3f
commit b6c3fd9a28
@@ -0,0 +1,112 @@
# ADR-007: React SSR for public rendering
- Status: Accepted
- Date: 2026-08-31
- Deciders: platform stream
- References: ADR index (section 70), ADR-001, Architecture wiki (sections 1, 19, 42),
Technology-Stack wiki (sections 5.2, 6, 7, 8)
## 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.
The public rendering pipeline is already documented: Fastify route → SiteResolver →
VisitorPreferenceResolver → ContentService → PageComposition + BlockRegistry →
ThemeResolver → React DOM server renderer → HTML (Architecture wiki §19), and the
v0.1 public surface exposes `GET /`, `GET /posts/:slug`, `GET /assets/*`,
`GET /media/*`, `GET /health/live`, `GET /health/ready` and `GET /admin/*` (§42).
This ADR is the focused record of the rendering choice at the end of that pipeline.
React is already the pinned rendering technology in the golden compatibility tuple:
React / React DOM 19.2.8, class C, in the runtime stack and golden tuple
(Technology-Stack §5.2, §7), and ADR-001 fixes the runtime as "Node.js 24 LTS with
Fastify 5, PostgreSQL 18, React 19 for server-rendered public components and a Vite
admin". The LTS strategy keeps React exact-pinned at 19.2.8, keeps React Server
Components out of v1, and keeps React out of persisted content formats
(Technology-Stack §6).
The workspace does not implement the public renderer yet — `apps/server` is the bare
Fastify health shell — so this ADR records the decision ahead of the code that
implements it, formalising what the Architecture and Technology-Stack wikis already
mandate: public pages are server-rendered first, React is a server-rendering detail,
and the site is not hydrating (§19, §19.1).
## Decision
EPPP renders all public pages with **React SSR for public rendering**: server-side
rendering through the React DOM server renderer, React 19.2.8 exact-pinned (class C,
golden tuple). Public pages are server-rendered first (§19); the pipeline terminates
in the React DOM server renderer producing HTML, with no client-side hydration of
core public pages — React is a server-rendering detail and the site is not hydrating
(§19.1). Core public Home/article pages target 0 bytes of EPPP JavaScript, and only
truly interactive features register client islands with an explicit activation mode
(§19.2). React Server Components are out of scope for v1, and React stays out of
persisted content formats (Technology-Stack §6).
The decision text — **React SSR for public rendering** — matches the ADR index entry
(ADR-007, section 70).
## Alternatives
- **Client-side rendering (SPA)** — rejected: public pages must work with JavaScript
disabled (§19.1 zero-JavaScript baseline); a browser-rendered SPA delivers no HTML
to first paint and contradicts the server-rendered-first pipeline (§19).
- **Static site generation (build-time SSG)** — rejected: content is connection-backed
PostgreSQL 18 (ADR-005) resolved per request with visitor preferences and
extension-owned content; build-time HTML would go stale against the canonical DB and
add a rebuild/redeploy cycle per content change.
- **SSR with full client hydration** — rejected for v1: core public pages target
0 bytes of EPPP JavaScript (§19.1); hydration would ship a client bundle to every
visitor when only a few interactive features need JS (§19.2 islands).
- **React Server Components (RSC)** — rejected for v1: the LTS strategy keeps RSC out
of v1 and out of persisted content formats (Technology-Stack §6); under the
zero-JavaScript baseline there is no client component tree to serve.
- **Template engine / string templating (e.g. Pug or Handlebars via @fastify/view)** —
rejected: the pipeline already terminates in the React DOM server renderer (§19),
themes resolve through the ThemeResolver into that renderer, and the golden tuple
pins React 19.2.8; a second rendering technology would duplicate work with no
benefit.
## Consequences
- Positive: server-rendered HTML works with JavaScript disabled, meeting the
zero-JavaScript baseline (§19.1); public pages get a fast first paint with no client
bootstrapping; metadata/SEO output is plain HTML; React is already in the golden
tuple (class C) so no new dependency or stack choice; one rendering path serves all
public pages, and themes/extensions compose through the documented pipeline
(PageComposition, BlockRegistry, ThemeResolver).
- Negative: SSR costs per-request CPU in the Node.js process; the React runtime must
load in the server process; rendering errors surface at request time rather than at
build time; public pages have no client interactivity without registered islands.
- Neutral: React stays a server-rendering detail — the site is not hydrating (§19.1);
browser JavaScript exists only on registered islands (§19.2); the decision constrains
the later client-side decisions (zero-JS public baseline and the client-island model,
ADR-009 and ADR-010).
## Operational impact
- One Node.js 24 process renders public HTML in-process via the React DOM server
renderer inside the single application image (ADR-001); no separate rendering
service or runtime build step.
- Public routes in the v0.1 surface (§42) return server-rendered HTML; core
Home/article pages ship 0 bytes of EPPP JavaScript (§19.1) — a measurable CI
invariant once the renderer lands.
- Rendering is stateless: horizontal scaling means more instances of the same image
behind the optional edge proxy; rendering load is part of the application process.
- React 19.2.8 is exact-pinned and class C (Technology-Stack §5.2, §7); upgrades flow
through the dependency lanes (ADR-026), and a React major is an ADR-recorded upgrade
program item (§7/§8).
- The renderer is not implemented yet — `apps/server` is the Fastify health shell — so
today rollback is reverting this documentation commit; once the renderer lands,
rollback is redeploying the previous image.
## Revisit trigger
- Revisit when a v0.1 public page cannot meet the zero-JavaScript baseline (§19.1)
with server rendering alone, or a required feature needs browser-side rendering at
scale.
- Revisit when React Server Components or full hydration is proposed for v1 —
Technology-Stack §6 explicitly keeps RSC out of v1.
- Revisit when the golden tuple's React pin moves to a new major (an ADR-recorded
upgrade program item, §7/§8) or React's support class changes.
- Revisit when visitor preferences or the v1.1 boundary contracts (ADR-027 to ADR-032)
demand a different rendering model — consistent with the Architecture §19 gates.