Compare commits

..
3 Commits
Author SHA1 Message Date
implementer e04e99b54c docs(adr): point ADR-006 at the in-repo ADR index (docs/adr/README.md)
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (pull_request) Successful in 1m7s
CI / Stage 4 — Unit tests (E00-S05-T01) (pull_request) Successful in 1m35s
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 1m10s
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
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (pull_request) Successful in 44s
Resolves the reviewer finding: ADR-006 referenced "ADR index (section 70)"
but no index file existed in the repository. The References line, the
Decision match sentence and the Revisit trigger §68 reference now name
docs/adr/README.md, so every "ADR index (section N)" claim resolves
inside the repository.
2026-08-31 00:52:47 +00:00
implementer ecbb9e94aa docs(adr): add repository ADR index mirroring the wiki ADR-Index page
Adds docs/adr/README.md as the in-repo copy of the ADR index (§70 of the
wiki ADR-Index page), including the "ADR-006 | Kysely contained inside DB
adapter" entry, the §46 six-sections mandate, the §71 architectural fitness
tests and the §68 architecture review gates. Committed ADRs in docs/adr/
must record a decision that matches their §70 row; the wiki page remains
the canonical index.
2026-08-31 00:52:47 +00:00
implementer 80fb9cf181 docs(adr): record Kysely containment decision as ADR-006
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (pull_request) Successful in 1m10s
CI / Stage 4 — Unit tests (E00-S05-T01) (pull_request) Successful in 1m41s
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 3m10s
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 45s
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (pull_request) Successful in 44s
2026-08-31 00:37:58 +00:00
5 changed files with 79 additions and 319 deletions
-87
View File
@@ -1,87 +0,0 @@
# 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
@@ -1,86 +0,0 @@
# 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
@@ -1,136 +0,0 @@
# 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.
@@ -3,8 +3,9 @@
- 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)
- References: ADR index (docs/adr/README.md, section 70), ADR-001, ADR-005,
Architecture wiki (sections 1, 4, 9), Engineering-Standards wiki
(sections 24, 25, 57)
## Context
@@ -49,6 +50,9 @@ 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.
The decision text — **Kysely contained inside DB adapter** — matches the ADR
index entry (ADR-006, section 70 of `docs/adr/README.md`).
## Alternatives
- **Kysely imported directly by domain/extension packages** — rejected: it
@@ -77,11 +81,10 @@ is a package-level rule, not a type-level one.
- 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).
exact-pinned single-manifest changes (ADR-026 lane); 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
@@ -99,7 +102,7 @@ is a package-level rule, not a type-level one.
(`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
reviewed under the dependency/upgrade lane (ADR-026); 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.
@@ -108,8 +111,9 @@ is a package-level rule, not a type-level one.
- 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.
API, and there is evidence (per the architecture review gates, ADR index
§68 of `docs/adr/README.md`) 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.
+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.