Compare commits
4
Commits
c1020353a0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d5c77e21d | ||
|
|
893707604d | ||
|
|
693976c289 | ||
|
|
b6c3fd9a28 |
@@ -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.
|
||||||
Reference in New Issue
Block a user