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.
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)
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]>
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]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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.
SET ROLE, or PostgreSQL roles.database.abi: 1. v8 is a hard cutover, like every manifest version. A v8 module withoutdatabasekeeps the legacyModuleDatabasefacade 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.query({ text, values, rowMode })returns{ command, rowCount, rows, fields }. Each module gets its own executor.Uint8Array, and arrays of these. NoDate, no implicit JSON, no identifiers.int8,numeric, temporal types, and JSON arrive as text, with session DateStyle, IntervalStyle, and TimeZone fixed.byteaarrives as aUint8Array.ModuleDatabaseError { code, sqlState? }with no SQL, parameters, or driver text, recognised structurally.AbortSignal.pg-proxy), a raw-SQL tagged template, and later Kysely.is(PgTable),getTableConfig, orPgColumnon their objects.packageFormatstays 1. ABI v1 packages omittransitional.databaseRuntime, and the verifier refuses one that carries it.Relationship to earlier ADRs
Pickfacade definition and the one-shared-services-object rule.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 verifypasses.🤖 Generated with Claude Code
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]>