Compare commits

...
13 Commits
Author SHA1 Message Date
kpcto 2d5c77e21d Merge pull request '[E01-S01-T05] ADR: Kysely containment' (#411) from feature/192 into main
CI / Stage 4 — Unit tests (E00-S05-T01) (push) Successful in 1m35s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (push) Successful in 1m1s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (push) Successful in 45s
CI / Stage 5 — Architecture tests (E00-S05-T01) (push) Successful in 3m8s
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (push) Successful in 1m8s
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (push) Successful in 45s
CI / Stage 2 — Typecheck (E00-S05-T01) (push) Successful in 1m9s
Reviewed-on: #411
2026-08-31 01:11:59 +00:00
kpcto 893707604d Merge pull request '[E01-S01-T06] ADR: React SSR' (#412) from feature/193 into main
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (push) Successful in 45s
CI / Stage 5 — Architecture tests (E00-S05-T01) (push) Successful in 3m8s
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (push) Successful in 1m8s
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (push) Successful in 46s
CI / Stage 2 — Typecheck (E00-S05-T01) (push) Successful in 1m9s
CI / Stage 4 — Unit tests (E00-S05-T01) (push) Successful in 1m35s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (push) Successful in 1m11s
Reviewed-on: #412
2026-08-31 01:11:44 +00:00
kpcto c1020353a0 Merge pull request '[E01-S01-T07] ADR: React/Vite admin' (#413) from feature/194 into main
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (push) Successful in 44s
CI / Stage 5 — Architecture tests (E00-S05-T01) (push) Successful in 3m14s
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (push) Successful in 1m10s
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (push) Successful in 46s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (push) Successful in 1m2s
CI / Stage 4 — Unit tests (E00-S05-T01) (push) Successful in 1m35s
CI / Stage 2 — Typecheck (E00-S05-T01) (push) Successful in 1m9s
Reviewed-on: #413
2026-08-31 01:11:33 +00:00
implementer edbf8cb7ea docs(adr): record React/Vite admin decision as ADR-008
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (pull_request) Successful in 44s
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (pull_request) Successful in 1m8s
CI / Stage 4 — Unit tests (E00-S05-T01) (pull_request) Successful in 1m40s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (pull_request) Successful in 1m5s
CI / Stage 2 — Typecheck (E00-S05-T01) (pull_request) Successful in 1m8s
CI / Stage 5 — Architecture tests (E00-S05-T01) (pull_request) Successful in 3m7s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (pull_request) Successful in 45s
Commits ADR-008 (React/Vite admin) for [E01-S01-T07], with the six mandated
sections (Context, Decision, Alternatives, Consequences, Operational impact,
Revisit trigger) per ADR index 46, and adds docs/adr/README.md as the
repository copy of the ADR index (section 70) so the decision-text match is
verifiable inside the repo. Documentation-only: no source, manifest,
lockfile, workflow or test changes.
2026-08-31 00:56:54 +00:00
implementer 693976c289 docs(adr): record Kysely containment decision as ADR-006
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (pull_request) Successful in 44s
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (pull_request) Successful in 1m9s
CI / Stage 4 — Unit tests (E00-S05-T01) (pull_request) Successful in 1m34s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (pull_request) Successful in 1m2s
CI / Stage 2 — Typecheck (E00-S05-T01) (pull_request) Successful in 1m9s
CI / Stage 5 — Architecture tests (E00-S05-T01) (pull_request) Successful in 3m8s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (pull_request) Successful in 45s
2026-08-31 00:55:38 +00:00
implementer b6c3fd9a28 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
2026-08-31 00:51:41 +00:00
kpcto b1a1c9bc86 Merge pull request '[E01-S01-T02] ADR: Node/TypeScript' (#407) from feature/189 into main
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (push) Successful in 1m8s
CI / Stage 4 — Unit tests (E00-S05-T01) (push) Successful in 1m35s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (push) Successful in 1m12s
CI / Stage 5 — Architecture tests (E00-S05-T01) (push) Successful in 3m10s
CI / Stage 2 — Typecheck (E00-S05-T01) (push) Successful in 1m22s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (push) Successful in 47s
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (push) Successful in 45s
2026-08-31 00:51:05 +00:00
kpcto 2a229bbf3f Merge pull request '[E01-S01-T03] ADR: Fastify' (#410) from feature/190 into main
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (push) Successful in 1m10s
CI / Stage 4 — Unit tests (E00-S05-T01) (push) Successful in 1m35s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (push) Successful in 1m8s
CI / Stage 2 — Typecheck (E00-S05-T01) (push) Successful in 1m11s
CI / Stage 5 — Architecture tests (E00-S05-T01) (push) Successful in 3m9s
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (push) Successful in 44s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (push) Successful in 44s
2026-08-31 00:43:50 +00:00
implementer b8f0abdb0d docs(adr): record Fastify 5 HTTP runtime decision as ADR-004
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
2026-08-31 00:34:08 +00:00
kpcto 768f009ee9 Merge pull request '[E01-S01-T04] ADR: PostgreSQL' (#408) from feature/191 into main
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (push) Successful in 1m7s
CI / Stage 4 — Unit tests (E00-S05-T01) (push) Successful in 1m48s
CI / Stage 5 — Architecture tests (E00-S05-T01) (push) Successful in 3m8s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (push) Successful in 1m3s
CI / Stage 2 — Typecheck (E00-S05-T01) (push) Successful in 1m10s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (push) Successful in 56s
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (push) Successful in 45s
2026-08-31 00:34:00 +00:00
bot-implementer dbd42d394d docs(adr): record PostgreSQL 18 sole canonical DB decision as ADR-005
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (pull_request) Successful in 1m9s
CI / Stage 4 — Unit tests (E00-S05-T01) (pull_request) Successful in 1m42s
CI / Stage 5 — Architecture tests (E00-S05-T01) (pull_request) Successful in 3m13s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (pull_request) Successful in 1m2s
CI / Stage 2 — Typecheck (E00-S05-T01) (pull_request) Successful in 1m9s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (pull_request) Successful in 46s
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (pull_request) Successful in 53s
2026-08-31 00:27:50 +00:00
implementer ee0c094398 docs(adr): record TypeScript 6.0.3 language decision as ADR-003
CI / Stage 4 — Unit tests (E00-S05-T01) (pull_request) Successful in 1m39s
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 3m9s
CI / Stage 2 — Typecheck (E00-S05-T01) (pull_request) Successful in 1m8s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (pull_request) Successful in 49s
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (pull_request) Successful in 1m10s
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (pull_request) Successful in 1m9s
2026-08-31 00:26:35 +00:00
implementer 1fba3d9f95 docs(adr): record Node.js 24 LTS runtime decision as ADR-002 2026-08-31 00:26:34 +00:00
8 changed files with 871 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
# ADR-002: Node.js 24 LTS runtime
- Status: Accepted
- Date: 2026-08-31
- Deciders: platform stream
- References: ADR index (section 70), Technology stack wiki (sections 5.2, 6, 7, 8), ADR-001
## Context
EPPP is a greenfield personal blogging platform (ADR-001) whose runtime is a
single modular-monolith application plus an optional worker, deployed as one
or more containers of one image. The whole platform — public server, admin
API, rendering pipeline, extension runtime, core services and background
jobs — executes on one runtime, so the runtime choice is a platform-wide,
hard-to-reverse decision with security, tooling and operational reach.
The workspace is a pnpm monorepo (pnpm 11.23.0) whose root manifest already
declares `engines.node: ">=24.0.0 <25.0.0"` with `engineStrict: true`, and
the CI pipeline installs and runs on Node 24 (E00-S01-T08). The technology
stack wiki classifies Node.js as support class A — a fixed upstream EOL date
— and pins the golden compatibility tuple to Node 24.19.0 LTS (Krypton), with
`node:24.19.0-bookworm-slim` as the application container base image.
## Decision
EPPP runs on the **Node.js 24 LTS** runtime line, exact-pinned to Node 24.19.0
LTS (Krypton) in the golden compatibility tuple and the container image. The
workspace root manifest restricts the engine to 24.x
(`engines.node: ">=24.0.0 <25.0.0"`), pnpm enforces it at install time
(`engineStrict: true`), and CI installs and runs on Node 24. The runtime is a
class A dependency: its upstream EOL (2028-04-30) drives the upgrade
schedule, and a move to a later major line (for example Node 26) is only
evaluated once that line is LTS and the compatibility suite passes, and then
only as a deliberate, ADR-recorded change. The decision text — **Node.js 24
LTS runtime** — matches the ADR index entry (ADR-002, section 70).
## Alternatives
- **Node 22 LTS (Jod)** — rejected: it is the previous LTS line and reaches
EOL before Node 24 (2027-04-30), shortening the runway for a platform whose
v1.1 boundary contracts (ADR-027 to ADR-032) extend past that date.
- **Node 26 (current, non-LTS at decision time)** — rejected: not LTS when
the decision was made; running a non-LTS line contradicts the class A
support posture (fixed EOL, security patching on an LTS cadence).
- **Node 24 with no exact pin (floating latest 24.x)** — rejected: a floating
minor would break the reproducibility of the golden compatibility tuple and
the container image; the line is pinned and minors move deliberately.
- **Node 24 LTS, exact-pinned 24.19.0** — chosen: an LTS major with a fixed
EOL, exact-pinned in the image and tuple, engine-restricted in the
manifest, and enforced in CI.
## Consequences
- Positive: an LTS line with a fixed upstream EOL (2028-04-30) gives a
predictable security-patching and upgrade horizon; the exact pin makes
builds and containers reproducible; the engine restriction rejects
unsupported Node versions at install time instead of failing at runtime.
- Negative: Node 24 API and behaviour become the platform floor — anything
needing a newer Node feature waits for the deliberate, ADR-recorded major
upgrade; minor upgrades inside 24.x still need the weekly dependency sweep
and full CI.
- Neutral: the runtime is shared by every module, so a future extraction
(ADR-001) keeps running on the same Node line until that module's runtime
needs diverge.
## Operational impact
- Application and worker containers run `node:24.19.0-bookworm-slim`; release
automation records the immutable image digest in the SBOM/release manifest.
- The supported runtime is enforced at install (`engineStrict: true` fails
installs on unsupported Node) and at test time (the node-engine suite
asserts the current runtime satisfies the 24.x range).
- CI installs and runs every stage on Node 24, so the committed pipeline is
the operational proof that the platform runs on the chosen line.
- Node 24 is patched on the LTS cadence; security-emergency updates go
through the focused lane (immediate, full CI), and the EOL date is the
deadline for the next major-line ADR.
## Revisit trigger
- Revisit when Node 26 becomes LTS and passes the compatibility suite, per
the LTS strategy — the decision to move majors is a new ADR, not a patch.
- Revisit on a security-emergency or patch-lane event that forces a minor
upgrade outside the weekly sweep, or if upstream announces a change to the
Node 24 support horizon.
- Revisit if a module's runtime needs (ADR-001 extraction) diverge from the
shared Node line and a second runtime enters the platform.
+86
View File
@@ -0,0 +1,86 @@
# ADR-003: TypeScript 6.0.3 language decision
- Status: Accepted
- Date: 2026-08-31
- Deciders: platform stream
- References: ADR index (section 70), Technology stack wiki (sections 5.2, 6, 7, 8), ADR-001
## Context
EPPP is a modular monolith (ADR-001) written in TypeScript across every
module boundary: `apps/`, `packages/` and `extensions/` are all compiled from
strict TypeScript against a shared base tsconfig (E00-S01-T10). The language
version is therefore a platform-wide decision: it fixes the type-system
features, the compiler behaviour and the toolchain (editor, build, CI
typecheck) every module sees.
The workspace already pins TypeScript exactly: the root manifest declares
`devDependencies.typescript: "6.0.3"` (a bare MAJOR.MINOR.PATCH, no semver
range), the committed lockfile resolves exactly one `typescript@6.0.3`, and
every workspace package resolves `Version 6.0.3` (E00-S01-T09). The
technology stack wiki classifies TypeScript as support class C — rolling,
exact-pinned, tested and upgraded deliberately — and pins 6.0.3 in the golden
compatibility tuple. TypeScript 7.0 reached GA in 2026-07 without a stable
programmatic API before 7.1, making 6.0 the bridge release.
## Decision
EPPP uses **TypeScript 6.0.3** as its language and compiler, exact-pinned in
the root manifest and the lockfile so every workspace package and every CI
typecheck runs the identical compiler. 6.0.3 is the baseline; a formal review
happens after TS 7.1 is stable (per the technology stack LTS strategy), and
any move to the 7.x line is a deliberate, ADR-recorded change. The decision
text — **TypeScript 6.0.3 pending TS7.1 ecosystem review** — matches the ADR
index entry (ADR-003, section 70).
## Alternatives
- **TypeScript 7.0 at GA (2026-07)** — rejected: it shipped without a stable
programmatic API before 7.1, which would put the compiler toolchain on an
unstable surface; 6.0 is the documented bridge.
- **TypeScript 6.x floating (caret/range)** — rejected: a range could resolve
to a different compiler than the one the golden tuple was tested with; the
exact pin is what makes typecheck deterministic across modules and CI.
- **TypeScript 5.x (previous major)** — rejected: it predates the 6.0 type
system and ecosystem position the platform was bootstrapped on; staying on
an older major only delays the bridge.
- **TypeScript 6.0.3 exact-pinned** — chosen: the bridge release, exact-pinned
and CI-verified, with a formal re-review once TS 7.1 stabilises the
programmatic API.
## Consequences
- Positive: one exact compiler version across apps, packages and extensions
makes typecheck results reproducible locally and in CI; the 6.0 bridge is a
known-good stepping stone to the 7.x line; strict mode across the workspace
stays uniform.
- Negative: the language feature set is fixed at 6.0.3 until the reviewed
upgrade; any tooling that needs the 7.x programmatic API waits for the TS
7.1 formal review.
- Neutral: upgrades inside the pinned major remain controlled by the
dependency sweep; the language choice is invisible to the deployed runtime
(TypeScript compiles away) but is enforced at build and typecheck time.
## Operational impact
- `pnpm install --frozen-lockfile` resolves exactly `typescript@6.0.3`; the
lockfile contains one resolved TypeScript entry, so no package can drift
onto another version.
- Every workspace `build`/`typecheck` script invokes the pinned compiler
(`pnpm exec tsc`), and the typescript-pin suite asserts each package
resolves `Version 6.0.3`.
- CI stage 2 (typecheck) runs `pnpm typecheck` across the workspace, so the
language pin is continuously verified on every pull request.
- A TypeScript upgrade is a class C lane change: patch/minor through the
sweep with full CI; the 7.x major is a programme item with its own ADR.
## Revisit trigger
- Revisit after TypeScript 7.1 is stable: the formal review (per the LTS
strategy) decides whether the platform moves to the 7.x line, recorded as a
new ADR.
- Revisit if a workspace package needs a type-system feature or toolchain
capability that 6.0.3 cannot provide, or if the ecosystem (editors, tools,
type packages) leaves the 6.0 bridge unsupported.
- Revisit if a module boundary contract (ADR-027 to ADR-032) becomes
unrepresentable in the pinned type system.
+136
View File
@@ -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.
@@ -0,0 +1,131 @@
# ADR-005: PostgreSQL 18 sole canonical DB
- Status: Accepted
- Date: 2026-08-31
- Deciders: platform stream
- References: ADR index (section 70), Architecture wiki (sections 1, 4, 9),
Technology-Stack wiki (sections 5.2, 5.4, 6, 7), Engineering-Standards wiki
(sections 24, 25, 58)
## Context
EPPP is a greenfield personal blogging platform owned by a single small
stream. The first visible release (v0.1) is intentionally small, but the
codebase ships explicit, versioned boundaries from day one — content types,
content blocks, page sections, themes, extensions, extension settings,
navigation, visitor preferences, database migrations, media storage,
background jobs, events and rendering — and all of these need durable,
transactional storage.
ADR-001 already records that the platform operates a single canonical
PostgreSQL database within a modular monolith; this ADR is the focused record
of the database choice itself. There is no v1 requirement for distributed
transactions, service discovery, message brokers or independent data stores,
so the database can be one engine, one instance, one consistency boundary.
The choice is already operationalised in the workspace:
- `compose.yaml` pins `postgres:18.6-bookworm` as the documented runtime
target (Technology-Stack §5.2/§5.4, golden tuple §7; E00-S03-T01), with a
`pg_isready` health gate and a named `db-data` volume for persistence.
- `packages/database-postgres` is the single workspace package allowed to
import `pg` and Kysely (dependency direction `database-postgres → core
ports → Kysely/pg`, Engineering-Standards §24); domain and extension
packages never import the driver.
- The migration runner coordinates on a PostgreSQL advisory lock
(`pg_advisory_lock`, E00-S03-T04) and gates app readiness on migration
completion (E00-S03-T06).
- Extensions are the growth mechanism; extension-owned tables and migrations
live in independent chains (`eppp_extension_migrations`) sharing the same
canonical database (ADR-001, Engineering-Standards §25).
PostgreSQL 18 is Class A in the runtime stack (fixed upstream EOL date,
Technology-Stack §5.1) with a documented support window to 2030-11-14; the
LTS strategy mandates always running the current minor, with a major upgrade
as a separate operator procedure (Technology-Stack §6).
## Decision
EPPP uses **PostgreSQL 18 as the sole canonical database**. One PostgreSQL 18
instance is the only database engine in the platform; it holds all core data,
and extension-owned tables live in independent migration chains inside the
same instance. `pg`/Kysely imports are isolated to `packages/database-postgres`
— the adapter boundary every database access passes through — and no other
database engine is introduced at v1.
The decision text — **PostgreSQL 18 sole canonical DB** — matches the ADR
index entry (ADR-005, section 70).
## Alternatives
- **SQLite** — rejected: the embedded, file-backed, single-writer model does
not fit the long-lived connection-backed server plus optional worker and
background jobs, and it offers no advisory-lock coordination for concurrent
migration runners; operational tooling for the Compose deployment model is
weaker than PostgreSQL's.
- **MySQL / MariaDB** — rejected: no v1 requirement needs their
differentiators; PostgreSQL provides the standards compliance, constraints,
JSONB and advisory locks that the migration runner and the versioned
contract model rely on, and a second SQL dialect would add cost without
benefit.
- **MongoDB / document store** — rejected: no v1 need for schemaless
documents; the platform's explicit versioned contracts (content types,
blocks, schemas validated by TypeBox/Ajv) benefit from a relational,
constraint-enforcing store.
- **Polyglot persistence (multiple engines)** — rejected: no v1 requirement
justifies the operational and cognitive cost; a single canonical database
keeps one transaction and consistency boundary (ADR-001).
- **Cloud-managed database (e.g. RDS/Aurora)** — rejected for v1: the primary
runtime model is Docker Compose with local parity; a managed service can be
revisited on evidence if hosting needs change.
- **Older PostgreSQL major / unpinned minor** — rejected: 18 is the
documented golden tuple; pinning the exact 18.6 minor makes the database
container reproducible (E00-S03-T01).
## Consequences
- Positive: one transaction and consistency boundary across the whole
platform; mature operational tooling (standard backups, `pg_isready` health
gates already in `compose.yaml`); advisory locks coordinate concurrent
migration runners; constraints and JSONB support the versioned contract
model; the adapter boundary keeps engine specifics contained in one package.
- Negative: a heavier operational footprint than embedded options — a
dedicated service with credentials, volume persistence and health/readiness
gating; schema changes demand migration discipline (expand/contract, never
edit a released migration — Engineering-Standards §25); the single canonical
database is a single point of failure (mitigated by volume persistence,
health gates and standard backup tooling).
- Neutral: the engine is encapsulated behind core ports, so moving to a
different engine later is a deliberate, evidence-based change rather than a
default; extensions share the canonical database with their own migration
chains.
## Operational impact
- A dedicated `db` service in `compose.yaml`, pinned to
`postgres:18.6-bookworm`, with a `pg_isready` healthcheck that gates app
start and a named `db-data` volume that persists across restart/recreate
(`docker compose down -v` resets it).
- Credentials come from the environment (`DATABASE_URL`, Compose
`POSTGRES_*` defaults), never embedded in the image (E00-S02-T08).
- One core migration chain plus extension-owned chains
(`eppp_extension_migrations`) run behind the advisory migration lock at
startup; readiness is reported only after migrations complete (E00-S03-T06).
- Backups use standard PostgreSQL tooling against the volume; version policy
follows the LTS strategy — always run the current minor, treat a major
upgrade as a separate operator procedure (Technology-Stack §6).
- At v1 there is no replication, multi-instance or sharding; scaling stays
vertical and the database remains the single shared store.
## Revisit trigger
- Revisit this ADR when a module or extension needs a genuinely different
data model (graph, search index, time-series) and there is evidence that
PostgreSQL features are insufficient — per the architecture review gates
(ADR index section 68); the adapter boundary keeps such a change contained.
- Revisit if the platform grows to multiple independent products or teams
requiring independent data stores or distributed transactions (shared with
the ADR-001 revisit trigger).
- Revisit if the golden tuple or Technology-Stack changes the database choice
or its support window — for example when PostgreSQL 18 EOL (2030-11-14)
approaches and a major upgrade becomes a scheduled operator procedure, or a
new major becomes the documented runtime target.
@@ -0,0 +1,118 @@
# ADR-006: Kysely contained inside DB adapter
- Status: Accepted
- Date: 2026-08-31
- Deciders: platform stream
- References: ADR-001, ADR-005, Architecture wiki (sections 1, 4, 9),
Engineering-Standards wiki (sections 24, 25, 57)
## Context
EPPP is a modular monolith (ADR-001) that keeps every module boundary
explicit, versioned and CI-enforced. ADR-005 already records the database
choice — PostgreSQL 18 as the sole canonical database — and with it the rule
that the `pg` driver and the Kysely query builder are isolated to
`packages/database-postgres`, the adapter boundary every database access
passes through. This ADR is the focused record of where the query builder
itself may live: the containment decision.
Kysely is a type-safe SQL query builder, not an ORM. It gives typed queries
and composable expressions on top of `pg`, but it still speaks SQL and a
dialect, and every package that imports it is coupled to that dialect and to
the adapter's implementation choices. The workspace already depends on the
containment: `packages/database-postgres` declares `kysely` 0.29.4 and `pg`
8.22.0 as its exact-pinned runtime dependencies (its only ones), imports them
in its source and re-exports the pieces the adapter is built on (E00-S03-T02),
and is the single workspace package whose source may import the driver — a
rule locked in by `tests/database-postgres-imports.test.mjs` in the
`database-postgres-imports` CI job and by the architecture fitness tests
FIT-010 (browser bundles cannot import server/DB packages) and FIT-011 (UI
cannot import Kysely/pg).
The dependency direction is already fixed: `database-postgres → core ports →
Kysely/pg` (Architecture wiki §9.1, Engineering-Standards §24). Domain and
extension packages talk to the database through core repository/port
interfaces, never through the query builder. What remains to be recorded is
that this is a standing architectural decision, not a temporary arrangement:
Kysely stays inside the adapter, and no other package may grow a Kysely or
`pg` import surface.
## Decision
EPPP keeps **Kysely contained inside DB adapter**. Kysely is an
implementation detail of `packages/database-postgres` — the single workspace
package allowed to import `pg`/Kysely (E00-S03-T02) — and the rest of the
workspace reaches the database exclusively through the adapter's and core
ports' APIs. No other workspace package may declare `kysely` or `pg` in its
manifest or import them directly in its source; the `database-postgres-imports`
CI gate plus FIT-010/FIT-011 enforce this on every PR. Kysely exists only
where the adapter lives — it is contained, not distributed, and the containment
is a package-level rule, not a type-level one.
## Alternatives
- **Kysely imported directly by domain/extension packages** — rejected: it
would leak SQL-dialect and query-builder concerns into domain and extension
code, defeat the single-owner boundary (E00-S03-T02), make FIT-011
impossible to honour, and couple business logic to the adapter's
implementation.
- **A dedicated shared query-layer package** — rejected: it would create a
second import surface for the driver stack, split ownership of the dialect,
and add a package whose only purpose is to widen the boundary; the adapter
already owns the typed surface callers need.
- **Full ORM (e.g. Prisma/Drizzle) instead of a query builder** — rejected:
schema is owned by the adapter's migration chains (ADR-005,
Engineering-Standards §25), and an ORM adds code generation and a schema
file that would couple domain models to storage; Kysely's typed builder is
sufficient and stays contained.
- **Raw `pg` everywhere / no query builder** — rejected: hand-written SQL
loses type safety and composability while still requiring the driver;
containing a query builder is strictly better than containing SQL strings.
- **Adapter re-exports Kysely for other packages to build queries** — rejected
(variant of the first alternative): even when routed through the adapter,
letting other packages compose Kysely queries would leak the dialect and
bypass the port/repository API that keeps domain code storage-agnostic.
## Consequences
- Positive: one owner for the whole driver/query-builder stack — upgrade,
dialect and typing decisions live in `packages/database-postgres` and are
exact-pinned single-manifest changes, so replacing or upgrading Kysely and
`pg` touches exactly one manifest; domain and extension code stays
driver-free, so FIT-010/FIT-011 hold; replacing Kysely or the dialect later
is a contained change behind the adapter boundary (consistent with the
ADR-005 encapsulation).
- Negative: the adapter's port/repository API must be designed well enough
that domain code never needs the query builder — the port API is mandatory,
not optional; Kysely conveniences (expression builders, plugin features) are
not available to callers outside the adapter; the adapter package grows as
the query surface grows.
- Neutral: Kysely remains a dependency of the adapter package only; contracts
between core ports and the adapter are plain TypeScript interfaces, so the
containment stays a package-level rule and never becomes a type-level one.
## Operational impact
- `kysely` and `pg` appear in exactly one manifest
(`packages/database-postgres/package.json`) and in exactly one package's
source; a static scan in `tests/database-postgres-imports.test.mjs`
(`database-postgres-imports` CI job) proves it on every PR, including a
mutation probe that fails when a driver import is injected elsewhere.
- Upgrading Kysely or `pg` is a single-manifest change inside the adapter,
reviewed under the workspace's dependency/upgrade lane; no other package's
manifest or code moves.
- No new runtime services, no schema changes, no deployment or configuration
impact: this ADR is documentation of an already-enforced boundary.
## Revisit trigger
- Revisit this ADR when a non-adapter package demonstrably needs
query-builder features that cannot be expressed through the adapter/port
API, and there is evidence (per the architecture review gates) that the
boundary costs more than containment saves.
- Revisit if the platform grows additional data stores or engines (shared with
the ADR-005 revisit trigger): a second engine may need its own contained
adapter, and the query-builder containment rule would extend per adapter.
- Revisit if the dependency-boundary enforcement changes — for example if
FIT-010/FIT-011 or the `database-postgres-imports` gate are relaxed or
re-scoped.
@@ -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.
+136
View File
@@ -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.
+65
View File
@@ -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.