Compare commits
5
Commits
693976c289
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d5c77e21d | ||
|
|
893707604d | ||
|
|
c1020353a0 | ||
|
|
edbf8cb7ea | ||
|
|
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.
|
||||
@@ -0,0 +1,136 @@
|
||||
# ADR-008: React/Vite admin
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2026-08-31
|
||||
- Deciders: platform stream
|
||||
- References: ADR index (section 70, `docs/adr/README.md`), ADR-001, ADR-007,
|
||||
Architecture wiki (sections 1, 4, 9, 11, 20, 42), Technology-Stack wiki
|
||||
(sections 5.2, 5.3, 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, core services,
|
||||
built admin assets and first-party extensions (Architecture §4). The v0.1 scope
|
||||
explicitly includes an "administration interface" (§1), and Architecture §20
|
||||
already fixes its shape: a **React client app built with Vite**, served by the
|
||||
same image, with `/admin/*` talking to the admin API under `/api/admin/v1/*` and
|
||||
an initial IA of Dashboard, Content→Posts, Home, Navigation, Appearance (active
|
||||
theme, theme settings), Extensions and Settings→Site. 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".
|
||||
|
||||
React and Vite are already the pinned toolchain in the golden compatibility
|
||||
tuple. React / React DOM 19.2.8 (class C, exact-pinned) is in the runtime stack
|
||||
and golden tuple (Technology-Stack §5.2, §7), and the build/admin/test toolchain
|
||||
is pinned to Vite 8.2.2, `@vitejs/plugin-react` 6.1.0, pnpm 11.23.0, Vitest
|
||||
4.1.10 and Playwright 1.62.1 (§5.3). 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 (§6); direct platform dependencies are exact-pinned
|
||||
with a committed lockfile (§8).
|
||||
|
||||
The workspace does not implement the admin app yet — `apps/server` is the bare
|
||||
Fastify health shell and there is no `apps/admin` (Architecture §9; the CI
|
||||
`build-apps` stage already globs `apps/**` so the admin is picked up when it
|
||||
lands). This ADR therefore records the decision ahead of the code that
|
||||
implements it, formalising what the Architecture and Technology-Stack wikis
|
||||
already mandate: the admin frontend is a React client app built with Vite and
|
||||
served by the same application image.
|
||||
|
||||
## Decision
|
||||
|
||||
EPPP builds the admin frontend with **React/Vite admin**: a React client
|
||||
application built with Vite — React 19.2.8 and Vite 8.2.2, exact-pinned class C
|
||||
dependencies from the golden compatibility tuple (Technology-Stack §5.2, §5.3,
|
||||
§7) — served by the same application image (ADR-001, Architecture §4). The admin
|
||||
lives under `/admin/*` and calls the admin API at `/api/admin/v1/*` (§20, §42).
|
||||
It is deliberately a separate concern from the public rendering pipeline:
|
||||
public pages stay server-rendered with no client hydration (ADR-007, §19.1),
|
||||
while the admin is a client-rendered SPA — the interactive management surface
|
||||
where JavaScript is expected and required. React Server Components stay out of
|
||||
v1 and React stays out of persisted content formats (Technology-Stack §6).
|
||||
The decision text — **React/Vite admin** — matches the ADR index entry
|
||||
(ADR-008, section 70).
|
||||
|
||||
## Alternatives
|
||||
|
||||
- **Server-rendered admin through the public React SSR pipeline** — rejected:
|
||||
the admin is an interactive management surface where JavaScript is expected;
|
||||
routing it through the zero-JavaScript, non-hydrated SSR pipeline (ADR-007,
|
||||
§19.1) would buy nothing — every admin page needs client interactivity — and
|
||||
would couple admin rendering to the public pipeline's per-request server
|
||||
rendering.
|
||||
- **Separate admin framework (e.g. Next.js, Angular, Vue SPA)** — rejected: the
|
||||
golden tuple pins React 19.2.8 (Technology-Stack §5.2, §7) and ADR-001 already
|
||||
records "a Vite admin"; a second frontend ecosystem would split the admin UI
|
||||
from the public components with no v1 requirement justifying it.
|
||||
- **Framework-less Vite / hand-rolled DOM** — rejected: the admin IA (§20) needs
|
||||
a component model for the content, theme, extension and settings screens;
|
||||
re-implementing state management and composition by hand duplicates what React
|
||||
already provides, and the golden tuple already pins React.
|
||||
- **Build-time static-site-generated admin** — rejected: the admin is an
|
||||
authenticated, connection-backed interface over the canonical database
|
||||
(ADR-005, ADR-019); build-time HTML would go stale against the canonical DB
|
||||
and add a rebuild/redeploy cycle per content change.
|
||||
- **Admin as a separate service or image** — rejected: ADR-001 fixes one
|
||||
application image containing built admin assets; a separate admin deployment
|
||||
would break the single-deployable-unit model and add service boundaries with
|
||||
no v1 benefit.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Positive: one frontend technology — React 19.2.8 — spans the public SSR
|
||||
pipeline (ADR-007) and the admin SPA, so patterns, components and the golden
|
||||
tuple pin are shared; Vite gives fast HMR during admin development and a
|
||||
static build that ships inside the same image (ADR-001); the admin is
|
||||
explicitly outside the public zero-JavaScript baseline (§19.1), so its
|
||||
interactivity does not conflict with the public surface; the exact-pinned
|
||||
toolchain (Vite 8.2.2, React 19.2.8, committed lockfile) keeps admin builds
|
||||
reproducible under the ADR-026 upgrade lanes.
|
||||
- Negative: the admin ships client JavaScript and a client runtime to
|
||||
authenticated admin users; a client-side build step joins the CI pipeline
|
||||
(the `build-apps` stage globs `apps/**`) and brings its own test tooling
|
||||
(Vitest, Playwright — Technology-Stack §5.3, §7); two rendering paths — public
|
||||
SSR and admin SPA — must be maintained, and the client/server contract at
|
||||
`/api/admin/v1/*` must stay versioned; the admin is an authenticated security
|
||||
surface (opaque DB-backed sessions, ADR-019) that the security baseline must
|
||||
keep covering (CSRF, session handling, authz).
|
||||
- Neutral: Vite is a build-time detail — what ships is static assets served
|
||||
behind `/admin/*` from the same image; the decision constrains the later
|
||||
client-side decisions (zero-JS public baseline and the client-island model,
|
||||
ADR-009 and ADR-010) to the public surface, not the admin.
|
||||
|
||||
## Operational impact
|
||||
|
||||
- `apps/admin` builds with Vite into static assets that are served by the same
|
||||
application image behind `/admin/*` (Architecture §20); there is no separate
|
||||
admin service, container or deployment (ADR-001).
|
||||
- `/admin/*` serves the SPA shell; the SPA reads and writes through the admin
|
||||
API at `/api/admin/v1/*` (§42), secured by the Fastify lifecycle and opaque
|
||||
DB-backed sessions (ADR-019).
|
||||
- The admin build runs in the CI `build-apps` stage — the pnpm `apps/**` glob
|
||||
picks `apps/admin` up automatically when the app lands — and admin behaviour
|
||||
is covered by Vitest unit suites and Playwright e2e (Technology-Stack §5.3,
|
||||
§7).
|
||||
- Admin traffic is authenticated and internal; the admin is not part of the
|
||||
public zero-JavaScript surface (§19.1) — client JavaScript is expected and
|
||||
required there — so no byte-budget applies to admin assets.
|
||||
- The admin is not implemented yet — today rollback is reverting this
|
||||
documentation commit; once the admin lands, rollback is redeploying the
|
||||
previous image (built admin assets live inside the image, so there is no
|
||||
separate asset deploy to coordinate).
|
||||
|
||||
## Revisit trigger
|
||||
|
||||
- Revisit when the admin grows a requirement the React/Vite SPA model cannot
|
||||
meet — for example a genuinely different client architecture or offline
|
||||
capability — with evidence per the architecture review gates (ADR index
|
||||
section 68).
|
||||
- Revisit when the golden tuple moves React or Vite to a new major — an
|
||||
ADR-recorded upgrade program item (Technology-Stack §7/§8, ADR-026 lane) — or
|
||||
changes the React pin's support class.
|
||||
- Revisit when the public client-side decisions (ADR-009, ADR-010) change the
|
||||
boundary between the public surface and the admin, or the admin API contract
|
||||
at `/api/admin/v1/*` needs a different shape.
|
||||
- Revisit when the single-image model (ADR-001) changes such that built admin
|
||||
assets must be served or deployed separately.
|
||||
@@ -0,0 +1,65 @@
|
||||
# ADR index (§70)
|
||||
|
||||
Repository copy of the ADR index from the wiki ADR-Index page (§70): the
|
||||
canonical list of architectural decisions, committed or planned. Every ADR
|
||||
committed to `docs/adr/` must record a decision that matches its row in
|
||||
section 70; the wiki page remains the canonical index.
|
||||
|
||||
| ADR | Decision |
|
||||
|---|---|
|
||||
| ADR-001 | Modular monolith |
|
||||
| ADR-002 | Node.js 24 LTS runtime |
|
||||
| ADR-003 | TypeScript 6.0.3 pending TS7.1 ecosystem review |
|
||||
| ADR-004 | Fastify 5 HTTP runtime |
|
||||
| ADR-005 | PostgreSQL 18 sole canonical DB |
|
||||
| ADR-006 | Kysely contained inside DB adapter |
|
||||
| ADR-007 | React SSR for public rendering |
|
||||
| ADR-008 | React/Vite admin |
|
||||
| ADR-009 | Zero-JS public baseline |
|
||||
| ADR-010 | Client-island model for optional public interactivity |
|
||||
| ADR-011 | JSON Schema + TypeBox + Ajv validation |
|
||||
| ADR-012 | EPPP Extension API hides framework internals |
|
||||
| ADR-013 | Node/Amber is a theme extension |
|
||||
| ADR-014 | Blog is a first-party content extension |
|
||||
| ADR-015 | Versioned block documents |
|
||||
| ADR-016 | Versioned page composition |
|
||||
| ADR-017 | Extension-owned migrations/tables |
|
||||
| ADR-018 | Docker Compose primary installation |
|
||||
| ADR-019 | Opaque DB-backed admin sessions |
|
||||
| ADR-020 | Separate anonymous preference identity |
|
||||
| ADR-021 | Local media storage through storage port |
|
||||
| ADR-022 | PostgreSQL jobs before external broker |
|
||||
| ADR-023 | No Redis initially |
|
||||
| ADR-024 | No microservices initially |
|
||||
| ADR-025 | Trusted build-time executable extensions in v1 |
|
||||
| ADR-026 | Exact dependency pinning + controlled upgrade lanes |
|
||||
| ADR-027 (v1.1) | `core.markdown` block restores Markdown authoring inside the block model |
|
||||
| ADR-028 (v1.1) | Hardened outbound fetch as a core service; extensions never fetch directly |
|
||||
| ADR-029 (v1.1) | Embed provider allowlist enforced in renderer and generated CSP |
|
||||
| ADR-030 (v1.1) | Native server-side SVG charts and diagrams with an accessibility contract |
|
||||
| ADR-031 (v1.1) | Theme renaming replaces trademark references |
|
||||
| ADR-032 (v1.1) | Day-one byte budgets; latency targets from measurement |
|
||||
|
||||
Every ADR contains: Context, Decision, Alternatives, Consequences, Operational
|
||||
impact, Revisit trigger (§46 E01-S01).
|
||||
|
||||
## Architectural fitness tests (§71)
|
||||
|
||||
- **Add Ledger/Paper:** create `theme-paper` extension → register
|
||||
manifest/tokens/assets → optionally override renderer slots → tests →
|
||||
include in build. Failure = editing Home/Post domain, core DB, auth, or
|
||||
`if (theme === "paper")` in core.
|
||||
- **Add Reading:** create `org.eppp.reading` → migrations → public route →
|
||||
admin contribution → Home section → settings → job(s) → content/block
|
||||
contributions. Failure = core learning seam/half-life/bookmark/link-health
|
||||
semantics.
|
||||
|
||||
## Architecture review gates (§68)
|
||||
|
||||
Gate A (end Sprint 1): publish a real post without core becoming
|
||||
blog/Amber-specific. Gate B (end Sprint 3): add a Home feature as an extension
|
||||
with no core edits. Gate C (end Sprint 4): a radically different theme runs
|
||||
without changing content/business logic. Gate D (end Sprint 5): visitor
|
||||
preference persists without coupling to auth or theme storage. Gate E (before
|
||||
public SDK): Extension API v1 proven enough to maintain. Gate F (Reading):
|
||||
Reading owns its whole domain without `if (readingEnabled)` in core.
|
||||
Reference in New Issue
Block a user