docs: propose ADR 0050, Module Database ABI v1 #68

Merged
Zayin_Admin merged 2 commits from docs/adr-0050-module-database-abi into main 2026-10-07 20:33:49 +00:00
Member

Proposes ADR 0050: Module Database ABI v1. Docs only.

What it decides

Core and modules exchange PostgreSQL SQL text, bound parameters, result rows with field metadata, and Core-owned transaction scopes. They never exchange ORM objects. Modules may use Drizzle, Kysely, raw SQL, or another module-side builder, and Core's own ORM becomes an implementation detail.

  • Not a security boundary. The ABI is a correctness and compatibility boundary. Modules remain trusted in-process code. The ADR makes no security claim for SQL parsing, execution-time prefix checks, SET ROLE, or PostgreSQL roles.
  • Opt-in with manifest v8 database.abi: 1. v8 is a hard cutover, like every manifest version. A v8 module without database keeps the legacy ModuleDatabase facade during the deprecation window. "Legacy module" means a v8 module on the legacy facade, never a v7 module. Already-built v7 packages must be rebuilt, and no v7/v8 manifest is parsed side by side.
  • The executor. query({ text, values, rowMode }) returns { command, rowCount, rows, fields }. Each module gets its own executor.
  • Parameters. null, boolean, number, bigint, string, Uint8Array, and arrays of these. No Date, no implicit JSON, no identifiers.
  • Results. A decoding table owned by Core, never pg's mutable global parsers.
    • int8, numeric, temporal types, and JSON arrive as text, with session DateStyle, IntervalStyle, and TimeZone fixed.
    • bytea arrives as a Uint8Array.
  • Errors. A sanitized ModuleDatabaseError { code, sqlState? } with no SQL, parameters, or driver text, recognised structurally.
  • Transactions. Callback-scoped and revoked when the callback settles. Core generates savepoint names and never allows overlapping savepoint scopes. There is no automatic retry and no AbortSignal.
  • Adapters live on the module side. Drizzle (via pg-proxy), a raw-SQL tagged template, and later Kysely.
  • Schema metadata is plain data for ABI v1 modules. Core never calls is(PgTable), getTableConfig, or PgColumn on their objects.
  • Migrations are unchanged. packageFormat stays 1. ABI v1 packages omit transitional.databaseRuntime, and the verifier refuses one that carries it.

Relationship to earlier ADRs

  • 0016: partly superseded, for ABI v1 modules only. That covers the Drizzle Pick facade definition and the one-shared-services-object rule.
  • 0007: amended, for ABI v1 modules only. Core inspects plain metadata instead of Drizzle tables, and every ownership rule is unchanged.
  • 0049: extended. This is the ABI it deferred.
  • 0048: related. The trust boundary is unchanged.

Accepted ADRs are not edited.

Changes

  • docs/architecture/decisions/0050-module-database-abi-v1.md: new, Proposed.
  • docs/architecture/decisions/README.md: index entry.
  • docs/architecture/platform-architecture.md: a one-sentence pointer from the packaging summary.

No runtime, SDK, manifest, packaging, migration, CI, or Signage changes.

Verification

  • pnpm verify passes.
  • A relative-link and anchor check of the changed docs passes.
  • I searched for stale or conflicting text. Existing guides still describe today's behaviour, which is accurate until the ADR is implemented.

🤖 Generated with Claude Code

Proposes **ADR 0050: Module Database ABI v1**. Docs only. ## What it decides Core and modules exchange **PostgreSQL SQL text, bound parameters, result rows with field metadata, and Core-owned transaction scopes**. They never exchange ORM objects. Modules may use Drizzle, Kysely, raw SQL, or another module-side builder, and Core's own ORM becomes an implementation detail. - **Not a security boundary.** The ABI is a correctness and compatibility boundary. Modules remain trusted in-process code. The ADR makes no security claim for SQL parsing, execution-time prefix checks, `SET ROLE`, or PostgreSQL roles. - **Opt-in with manifest v8 `database.abi: 1`.** v8 is a hard cutover, like every manifest version. A v8 module without `database` keeps the legacy `ModuleDatabase` facade during the deprecation window. "Legacy module" means a v8 module on the legacy facade, never a v7 module. Already-built v7 packages must be rebuilt, and no v7/v8 manifest is parsed side by side. - **The executor.** `query({ text, values, rowMode })` returns `{ command, rowCount, rows, fields }`. Each module gets its own executor. - **Parameters.** null, boolean, number, bigint, string, `Uint8Array`, and arrays of these. No `Date`, no implicit JSON, no identifiers. - **Results.** A decoding table owned by Core, never pg's mutable global parsers. - `int8`, `numeric`, temporal types, and JSON arrive as text, with session DateStyle, IntervalStyle, and TimeZone fixed. - `bytea` arrives as a `Uint8Array`. - **Errors.** A sanitized `ModuleDatabaseError { code, sqlState? }` with no SQL, parameters, or driver text, recognised structurally. - **Transactions.** Callback-scoped and revoked when the callback settles. Core generates savepoint names and never allows overlapping savepoint scopes. There is no automatic retry and no `AbortSignal`. - **Adapters live on the module side.** Drizzle (via `pg-proxy`), a raw-SQL tagged template, and later Kysely. - **Schema metadata is plain data for ABI v1 modules.** Core never calls `is(PgTable)`, `getTableConfig`, or `PgColumn` on their objects. - **Migrations are unchanged.** `packageFormat` stays 1. ABI v1 packages omit `transitional.databaseRuntime`, and the verifier refuses one that carries it. ## Relationship to earlier ADRs - **0016:** partly superseded, for ABI v1 modules only. That covers the Drizzle `Pick` facade definition and the one-shared-`services`-object rule. - **0007:** amended, for ABI v1 modules only. Core inspects plain metadata instead of Drizzle tables, and every ownership rule is unchanged. - **0049:** extended. This is the ABI it deferred. - **0048:** related. The trust boundary is unchanged. Accepted ADRs are not edited. ## Changes - `docs/architecture/decisions/0050-module-database-abi-v1.md`: new, Proposed. - `docs/architecture/decisions/README.md`: index entry. - `docs/architecture/platform-architecture.md`: a one-sentence pointer from the packaging summary. No runtime, SDK, manifest, packaging, migration, CI, or Signage changes. ## Verification - `pnpm verify` passes. - A relative-link and anchor check of the changed docs passes. - I searched for stale or conflicting text. Existing guides still describe today's behaviour, which is accurate until the ADR is implemented. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ekklesia-bot added 1 commit 2026-10-07 20:19:15 +00:00
docs: propose ADR 0050, Module Database ABI v1
CI / verify (pull_request) Successful in 57s
CI / infrastructure-smoke (pull_request) Successful in 35s
CI / migration-smoke (pull_request) Successful in 1m50s
CI / image-build (pull_request) Successful in 22s
CI / deploy-smoke (same-origin) (pull_request) Successful in 49s
CI / deploy-smoke (split) (pull_request) Successful in 1m31s
759872834d
Core and modules exchange PostgreSQL SQL text, bound parameters, result
rows with field metadata, and Core-owned transaction scopes. Never ORM
objects. A module may use Drizzle, Kysely, raw SQL, or another
module-side builder. Core's own ORM becomes an implementation detail.

ADR 0050 decides:
- an explicit correctness/compatibility boundary, not a security
  sandbox: no claim for SQL parsing, execution-time prefix checks,
  SET ROLE, or PostgreSQL roles;
- opt-in through manifest version 8 `database.abi: 1`, a hard cutover
  like every manifest version; a version 8 module without `database`
  keeps the legacy ModuleDatabase facade during the deprecation window;
- the executor: query({ text, values, rowMode }) returning
  { command, rowCount, rows, fields }, one executor per module;
- a small parameter set (no Date, no implicit JSON, no identifiers);
- a Core-owned decoding table that loses no precision and does not
  depend on time zones (int8, numeric, temporal, and JSON as text;
  bytea as Uint8Array);
- a sanitized ModuleDatabaseError { code, sqlState? }, recognised
  structurally;
- callback-scoped transactions and Core-named savepoints, revoked when
  their callback settles;
- module-side adapters (Drizzle via pg-proxy, raw SQL, later Kysely);
- plain schema metadata for ABI v1 modules instead of Drizzle objects;
- migrations unchanged; packageFormat stays 1; ABI v1 packages omit
  transitional.databaseRuntime.

It partly supersedes ADR 0016 and amends ADR 0007 for ABI v1 modules,
and extends ADR 0049. Status Proposed. Accepted ADRs are unchanged. No
runtime, SDK, manifest, packaging, migration, or Signage changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Zayin_Admin added 1 commit 2026-10-07 20:27:31 +00:00
docs: ADR 0050, tighten transaction scope, single-statement, and error rules
CI / verify (pull_request) Successful in 56s
CI / infrastructure-smoke (pull_request) Successful in 36s
CI / migration-smoke (pull_request) Successful in 1m52s
CI / image-build (pull_request) Successful in 22s
CI / deploy-smoke (same-origin) (pull_request) Successful in 49s
CI / deploy-smoke (split) (pull_request) Successful in 1m29s
11e2495484
Three narrow clarifications to the proposed contract. No other decision
changes.

- Outstanding work: module code awaits every operation before its
  transaction or savepoint callback returns. A callback that settles
  with work outstanding is a scope-usage error and never commits. Core
  may let started operations settle before rolling back and releasing
  the connection, but waiting never turns into a commit. Executors stay
  revoked once their scope has finished.
- One statement per query() call: a correctness and API rule, not a
  security boundary. It is upheld by a reliable driver or protocol
  mechanism, never a home-grown SQL parser. An implementation that
  cannot uphold it stops and reports instead of weakening the ADR.
- Errors: `code` is a stable Ekklesia-owned category and always present.
  `sqlState` is optional, present only when PostgreSQL supplied one.
  ABI-detected failures (closed scope, invalid parameter, invalid
  option) carry a code and no SQLSTATE.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Zayin_Admin merged commit d43031997b into main 2026-10-07 20:33:49 +00:00
Zayin_Admin deleted branch docs/adr-0050-module-database-abi 2026-10-07 20:33:49 +00:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Zayin/Ekklesia#68