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
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
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user