diff --git a/docs/architecture/decisions/0050-module-database-abi-v1.md b/docs/architecture/decisions/0050-module-database-abi-v1.md new file mode 100644 index 0000000..26bc0ab --- /dev/null +++ b/docs/architecture/decisions/0050-module-database-abi-v1.md @@ -0,0 +1,501 @@ +# 0050: Module Database ABI v1: modules and Core exchange SQL, never ORM objects + +- Status: Proposed +- Date: 2026-10-07 +- Supersedes (partial): [0016](0016-module-database-access.md) — for modules that declare ABI v1, + only its definition of the module database service as `Pick`, a facade over + Core's Drizzle instance, and its rule that every module receives the same `services` object. + One pool, no pool or connection string for modules, no database access during `register`, the + shutdown order, handler error sanitization, and the trust boundary remain accepted and unchanged. + Modules that do not declare ABI v1 keep ADR 0016's facade until it is removed (see + [Compatibility and deprecation](#compatibility-and-deprecation)). +- Amends: [0007](0007-module-owned-drizzle-schemas.md) — for modules that declare ABI v1, the + schema definition Core inspects is plain Ekklesia-owned metadata instead of Drizzle table objects. + Table namespacing, identifier rules, uniqueness, same-module foreign keys, atomic aggregation, and + sanitized discovery errors remain accepted and unchanged. +- Extends: [0049](0049-module-package-v1.md) — this is the Module Database ABI that ADR 0049 deferred + and anticipated. ABI v1 packages carry no `transitional.databaseRuntime`. `packageFormat` stays 1. +- Related: [ADR 0006](0006-versioned-capabilities-and-compiled-module-runtime.md), + [ADR 0008](0008-module-owned-migration-contracts.md), + [ADR 0009](0009-transactional-module-migrations.md), + [ADR 0015](0015-static-module-http-routing-and-runtime-lifecycle.md), + [ADR 0021](0021-modules-as-hostable-products.md), + [ADR 0047](0047-module-ci-harness.md), + [ADR 0048](0048-generic-image-and-installed-module-storage.md) + +## Context + +ADR 0016 gave module code database access through `ModuleDatabase`, which is +`Pick`. Core implements it as a frozen object whose eight methods are +bound to Core's own `drizzle(pool)` instance. + +ADR 0048 then took modules out of the platform image, and ADR 0049 made each module release a +self-contained package that bundles its own copy of Drizzle. The two copies now meet at run time, +and an investigation of the current code (Drizzle 0.45.2) found the coupling runs deeper than a +type: + +- **Module objects cross into Core's ORM.** A module builds queries from table, column, and `SQL` + objects made by its own Drizzle copy. Core's Drizzle instance then compiles and decodes them. + This only works while the two copies agree on internal symbols and entity kinds, which is why ADR + 0049 records the module's Drizzle version under `transitional.databaseRuntime` and refuses a + mismatch. +- **Core's ORM objects cross into the module.** `transaction` hands module code Core's live + `NodePgTransaction`. At run time that object is a full Drizzle database: its session, dialect, + relational query builder, and `rollback()` are all reachable, whatever the `Pick` type says. A + reference kept after the callback settles still points at a connection that has returned to the + pool. +- **Result decoding is split between two ORMs.** Core's Drizzle session overrides `pg`'s parsers + for each query, so temporal values arrive as raw text. Then the module's column objects convert + them. Neither half is an Ekklesia contract. `pg`'s default parsers are also process-global and + mutable, and some lose information: `jsonb` numbers beyond double precision, and `numeric[]` + elements, are parsed to JavaScript numbers. +- **Errors are ORM and driver shapes.** A module that needs to recognize a PostgreSQL condition + reads it from whatever object Drizzle or `pg` throws. Drizzle 0.45 wraps driver errors in its own + error type, whose message carries the SQL text and the parameters. +- **Schema validation needs Drizzle objects.** Module validation, Doctor, and the package verifier + import a module's schema entrypoint and inspect it with Drizzle's `is(…, PgTable)`, + `getTableConfig`, and `PgColumn`. That is the same cross-copy dependency, at validation time. Core + extracts only plain facts from it: table names and foreign-key target tables. + +Two findings show the dependency is unnecessary: + +- **Migration execution is already independent of the ORM runtime.** Migration discovery reads SQL + files and JSON metadata, and the runner executes SQL text. Neither imports Drizzle. Drizzle Kit + snapshots are validated metadata; they are not needed to apply a migration. +- **The module side can compile to SQL alone.** Drizzle and Kysely both compile a query to + PostgreSQL text with `$n` placeholders and an ordered parameter list. Drizzle 0.45.2's own + `pg-proxy` driver, given only a callback that receives SQL text, parameters, and an array-or-object + row mode, ran `select`, `update`, and `insert … returning`, and decoded `timestamptz` and `int8` + text with the module's own column definitions. That driver has no transactions, which a + module-side adapter has to supply. + +Signage 0.1.2, the first production module, uses `select`, `insert`, `update`, `delete`, +`transaction`, one `returning`, and SQL expressions in `set` clauses. It uses no `execute`, joins, +row locks, isolation options, or result row counts. None of its usage needs a Core ORM object. It +recognizes a uniqueness conflict by reading SQLSTATE `23505` from the thrown error. A stable, +Ekklesia-owned place for that code is desirable regardless of which ORM either side uses. (This +paragraph is motivation, not part of the decision.) + +## Decision + +### The goal: the ORM is an implementation detail on both sides + +Ekklesia defines a stable, Ekklesia-owned **Module Database ABI** between Core and modules. + +- **The boundary is PostgreSQL itself.** Exactly these concepts cross it: + - SQL text; + - ordered, bound parameter values; + - result rows and their metadata; + - transaction scopes that Core controls. +- **Core never receives or depends on a module's ORM or query-builder object.** That includes: + - Drizzle tables, columns, `SQL` objects, query builders, and transactions; + - Kysely objects; + - any other third-party ORM or runtime object. +- **Modules never receive Core's ORM objects, driver objects, or connections.** +- **Each side chooses its own tools.** A module may use Drizzle, Kysely, raw SQL, or any module-side + query builder that produces PostgreSQL text and parameters. Core may keep Drizzle, or replace it, + without any module noticing. + +This ADR defines **Module Database ABI version 1**. The TypeScript names below are illustrative; the +concepts and the semantics are the contract. + +### A correctness boundary, not a security boundary + +**The Module Database ABI is a correctness and compatibility boundary. It is not a security +sandbox.** Installed modules remain trusted backend code running inside the API process with its +database role (ADR 0006, ADR 0016, ADR 0048). + +Nothing in this ADR claims, and nothing built from it may claim, that the following can stop +malicious module code from reaching data that is not its own: + +- the ABI itself; +- parsing or inspecting SQL; +- checking table prefixes when statements execute; +- `SET ROLE`; +- PostgreSQL roles. + +A module sends arbitrary SQL by design, including SQL that changes its role back. It also runs +in-process, where it can reach the network and the environment without the ABI. + +Table ownership remains what ADR 0007 made it: a static correctness check of declared tables and +foreign keys, run during validation. The ABI keeps that check honest by removing the ORM objects it +used to depend on. It adds no runtime enforcement. + +### Declaring the ABI: manifest version 8 + +A module opts into the ABI explicitly, in its static manifest. The manifest is strict and changes by +hard cutover (ADR 0006, ADR 0014, ADR 0021, ADR 0029), so the declaration arrives with +**manifest version 8**. Conceptually: + +```json +{ + "manifestVersion": 8, + "database": { "abi": 1 } +} +``` + +- **`database.abi: 1`** means the module receives a Module Database ABI v1 executor. It never + receives the legacy facade. +- **No `database` field** means the module receives ADR 0016's legacy `ModuleDatabase` facade + during the deprecation window. +- **Any other ABI value** fails validation. A platform that does not implement a declared ABI refuses + the module rather than guessing. +- **Version 8 is a hard cutover, like every manifest version before it.** Core parses exactly one + manifest version; it never parses version 7 and version 8 side by side. + - An existing source module moves to version 8, and repacks, without changing any database code. + - A package already built with a version 7 manifest must be rebuilt. + - Module Package v1 has only just landed, and no published ecosystem depends on version 7 + packages, so this costs a rebuild and nothing more. + +In this ADR, a **legacy module** means a module with a version 8 manifest and no `database` +declaration, which therefore uses the legacy facade. It never means a module with a version 7 +manifest. + +Because the declaration is static, Core, the packager, and the verifier know a module's database +contract without loading its code. Because Core enforces it, an ABI v1 module cannot reach the +legacy facade, so nothing at run time can depend on Drizzle agreement. + +The rest of manifest version 8 (field placement, validation messages, the template, and the +cutover itself) is an implementation detail of the change that introduces it. + +### The executor + +An ABI v1 module's database service is an **executor**. Conceptually: + +```ts +query({ text, values, rowMode }) → { command, rowCount, rows, fields } +transaction(callback, options) → the callback's result +``` + +- **`text`** is exactly one PostgreSQL statement, using `$1 … $n` placeholders. Statement text that + holds several statements is not part of the ABI, and Core may reject it. +- **`values`** is the ordered list of bound parameters (see + [Parameters](#parameters)). Values are always bound and never interpolated into the text. +- **`rowMode`** is `"object"` or `"array"`. + - `"object"` rows are keyed by column name. + - `"array"` rows are positional, in `fields` order. An ORM that selects the same column name twice, + as joins do, needs this mode. + - Core rejects an object-mode result that has duplicate column names, rather than silently + dropping one. +- **`command`** is PostgreSQL's command tag word, such as `SELECT`, `INSERT`, `UPDATE`, or `DELETE`. +- **`rowCount`** is the number of rows PostgreSQL reports as processed, or `null` when it reports + none. +- **`rows`** are the result rows, decoded as in [Results](#results). `RETURNING` rows are ordinary + rows. +- **`fields`** are the ordered column metadata, present even when there are no rows: at least each + column's name and its PostgreSQL type OID. + +The executor contains no ORM type, AST, query object, or schema object, and accepts none. + +**One executor per module.** Each ABI v1 module receives its own executor, bound to its module id. +The module's `start` and `stop` hooks and its route handlers all receive that same object, which +keeps ADR 0016's guarantee that lifecycle code and handlers share one database service. Different +modules get different executors, so Core can attribute every statement to the module that issued it. + +**Observability belongs to Core.** Core may record per-module timings, command tags, row counts, and +error codes. It never writes SQL text or parameter values to logs, diagnostics, Doctor output, or +HTTP responses. + +**Session state is not part of the ABI.** Top-level `query` calls are separate autocommit statements +that may run on different pooled connections. A module must not rely on session-level state, and +must not change it. That includes: + +- session `SET`; +- temporary tables; +- named prepared statements; +- `LISTEN`; +- session advisory locks. + +Settings that must hold for one unit of work use `SET LOCAL` inside a transaction. Core may reset or +discard a connection that a module changed. `COPY`, cursors, streaming results, and named prepared +statements are not part of v1. + +### Parameters + +The ABI deliberately accepts a small set of PostgreSQL wire-safe values: + +| Value | Sent as | +| --- | --- | +| `null` | SQL `NULL` | +| `boolean` | `true` / `false` | +| `number` | its JavaScript decimal text; `NaN`, `Infinity`, and `-Infinity` as PostgreSQL's `NaN`, `Infinity`, and `-Infinity` | +| `bigint` | its decimal text | +| `string` | the text, unchanged | +| `Uint8Array` | `bytea` | +| arrays of the above | a PostgreSQL array, nested for more dimensions | + +Everything else is refused with a sanitized error before anything reaches PostgreSQL, rather than +being coerced. In particular: + +- **`Date` is not an ABI parameter.** A JavaScript `Date` is an instant. Whether it should become a + `timestamp` or a `timestamptz`, and in which time zone, depends on a column type the ABI cannot + see. The module-side adapter converts it to explicit text first; for example, Drizzle's timestamp + columns already send ISO-8601 text. +- **Plain objects are not implicit JSON.** A module or adapter encodes JSON explicitly, as a string, + and the SQL casts it where needed. +- **Identifiers are never parameters.** Table and column names are part of the module's compiled + SQL text. The ABI provides no identifier interpolation. + +### Results + +Core owns one documented decoding table for ABI v1. It does not depend on `pg`'s global parser +registry, which module code could change, or on any ORM. Values that a generic conversion could +change, or change silently by time zone, arrive in a stable raw form: + +| PostgreSQL type | Arrives as | +| --- | --- | +| any `NULL` | `null` | +| `bool` | `boolean` | +| `int2`, `int4`, `oid` | `number` | +| `int8` | `string`, its exact decimal text | +| `float4`, `float8` | `number`, including `NaN` and `±Infinity` | +| `numeric` / `decimal` | `string`, its exact text (`NaN` included) | +| `text`, `varchar`, `bpchar`, `name`, `uuid` | `string` | +| `date`, `timestamp`, `timestamptz`, `time`, `timetz`, `interval` | `string`: PostgreSQL's ISO text output | +| `json`, `jsonb` | `string`: the JSON text, not parsed | +| `bytea` | `Uint8Array` | +| arrays of any of the above | JavaScript arrays (nested for more dimensions), each element decoded by the same rule, with `null` elements as `null` | +| any other type | `string`: PostgreSQL's text output | + +- **Temporal text is fixed for every module.** Core fixes the session's `DateStyle` (ISO), + `IntervalStyle` (`postgres`), and `TimeZone` (UTC) on every connection that serves the ABI. The + same instant therefore always produces the same text, whatever the server's or the host's local + zone. +- **`bytea` has one representation.** A value is a `Uint8Array`. A module must not rely on it being + any particular subclass. +- **Higher-level conversion belongs to the module side.** Converting text to `Date`, `numeric` to a + decimal type, `int8` to `bigint` or `number`, and JSON text to values is the adapter's job, or the + module's. The Drizzle column types Signage uses already accept exactly these raw forms. + +Changing how any type arrives, including decoding a type that arrives as text today, requires a new +ABI version. + +### Errors + +Every failure the executor reports is a Core-owned, sanitized **`ModuleDatabaseError`**. +Conceptually: + +```ts +ModuleDatabaseError { code, sqlState? } +``` + +- **`code`** is a small, stable, Ekklesia-defined set. It covers at least: + - the statement failed in PostgreSQL; + - a parameter or option was refused; + - the scope is closed; + - the transaction could not be completed. + + The exact names are an implementation detail. +- **`sqlState`** is PostgreSQL's five-character SQLSTATE, present when PostgreSQL reported one. + Modules legitimately branch on stable conditions such as `23505` (unique violation), `23503` + (foreign-key violation), and `40001` (serialization failure). +- **It never carries:** + - SQL text or parameter values; + - the driver's message, detail, hint, constraint, table, or column names; + - a stack, a cause, connection details, or any Core, `pg`, or Drizzle object. +- **Modules recognize it structurally** (its name and its `code`), never with `instanceof`. A module + bundles its own copy of the SDK (ADR 0049), so class identity across the boundary is not a + contract. + +This replaces modules depending on `DrizzleQueryError`, `pg`'s `DatabaseError`, or any other driver +or ORM error shape. An uncaught `ModuleDatabaseError` in a route handler is sanitized at the +handler boundary exactly as before (ADR 0015, ADR 0016). + +### Transactions + +Transactions are **callback-scoped**, and Core owns the connection throughout: + +```ts +transaction(async (tx) => { … }, { isolation, readOnly }) +``` + +- **Starting.** Core takes a connection from its pool and begins a transaction with the requested + options. It then calls the callback with `tx`, a Module Database ABI v1 executor scoped to that + transaction alone. No `pg` client and no Core ORM transaction is ever exposed. +- **Commit and rollback.** Core commits when the callback's promise resolves, and rolls back when it + rejects. The transaction call then rejects with the callback's error. There is no `rollback()` + method: to roll back, the module throws. +- **Unfinished queries.** Before committing or rolling back, Core waits for every query the scope + has already started. If one of them fails after the callback resolved, Core rolls back and reports + the failure. +- **Revocation.** When the callback settles, `tx` is revoked. Any later call on it fails with the + closed-scope `ModuleDatabaseError` and sends nothing to PostgreSQL. A `tx` that escapes its + callback can never run a statement on a connection that has gone back to the pool. +- **Options.** `isolation` is `read committed`, `repeatable read`, or `serializable`. `readOnly` is a + boolean. An unknown option or value is refused, never ignored. `deferrable` is reserved for a + later version. +- **No automatic retries.** Core never retries a transaction. A callback may have side effects + outside the database, so retrying on `40001` is the module's decision. +- **Failed statements.** PostgreSQL semantics apply unchanged. After a failed statement the + transaction is aborted, and later statements fail until the scope ends. A module that wants to + continue after a failure uses a savepoint. + +**Savepoints** are callback-scoped too: + +```ts +tx.savepoint(async (sp) => { … }) +``` + +- **Naming.** Core generates every savepoint identifier. Modules never provide one. +- **Release and rollback.** A resolved callback releases the savepoint. A rejected one rolls back + to it, releases it, and rejects with the callback's error. +- **One active scope.** Only the innermost open scope may run statements. While a savepoint is + open, its parent executor refuses statements and new savepoints, so savepoint scopes can never + overlap. +- **Revocation.** `sp` is revoked when its callback settles, exactly like `tx`. + +Nested transactions are savepoints. No nested-transaction API exposes a driver or ORM object. + +**Timeouts and cancellation stay with Core.** The existing per-statement bounds of the pool +(`statement_timeout` and `query_timeout`) apply to every ABI statement. Core may also bound how long +a transaction stays open. Modules cannot change Core's timeout policy through the ABI. `AbortSignal` +and module-initiated cancellation are not part of v1. + +### Module-side adapters + +The ABI is ORM-neutral. The Module SDK may ship adapters **on the module side**, bundled into each +module like the rest of the SDK (ADR 0049): + +- **A Drizzle adapter.** It turns the executor into a Drizzle database the module's own tables work + with. It may use Drizzle's `pg-proxy` driver, mapping row-array queries to `rowMode: "array"` and + the rest to `"object"`. It supplies `transaction` by opening an ABI transaction and binding a fresh + Drizzle instance to `tx`. +- **A raw-SQL helper.** A tagged template that turns interpolations into placeholders and values. +- **Later, a Kysely adapter.** + +Adapter types and query-builder types are not part of the ABI. Core never learns, and never needs +to learn, that a module used Drizzle. A module may bundle any Drizzle version its adapter supports. + +### Schema metadata + +For an ABI v1 module, Core validates table ownership from **plain, Ekklesia-owned metadata**, not +from ORM objects. Conceptually: + +```yaml +tables: + - name: care_team_members + foreignKeys: + - table: care_team_teams +``` + +- **What it is.** Plain data: strings, arrays, and records, with no functions or class instances. + Core validates its structure strictly. +- **What Core checks.** It is enough for the checks ADR 0007 already makes: + - the module namespace and identifier rules; + - uniqueness; + - foreign keys that stay within the module and target its declared tables; + - atomic, deterministic aggregation with sanitized issues. +- **What Core never does.** For ABI v1 modules, Core does not call Drizzle's `is(…, PgTable)`, + `getTableConfig`, or `PgColumn` checks on module-provided objects, and does not import an ORM to + understand the metadata. +- **How a module produces it.** A Drizzle helper in the Module SDK may derive it from the module's + own Drizzle tables, on the module side. A Kysely or raw-SQL module may write it by hand or produce + it another way. +- **What it promises.** The metadata is a declaration checked for consistency. It is exactly as + strong a correctness check as today's, and no stronger: the database enforces columns when the + migration runs. + +Legacy modules keep the existing Drizzle-object path while legacy support exists. Where the metadata +lives (in the schema entrypoint's default export or in a static file) is decided by the change that +implements it. Either way, it must be readable without Core importing an ORM. + +### Migrations are unchanged + +Migration discovery, validation, and execution stay as ADR 0008, ADR 0009, and ADR 0048 define +them. The runner already executes SQL without an ORM runtime. Migrations stay forward-only and +explicit, and history is retained for absent modules. + +The Drizzle Kit migration file format (journal, snapshots, and `_ekklesia.json`) remains the module +migration contract. Whether that build-tool format should become Ekklesia-owned is a separate +question, and this ADR does not change it. + +### Module Package v1 + +ADR 0049's `packageFormat` stays **1**. For a module that declares ABI v1: + +- **Drizzle is allowed in the bundle.** It is a module-side implementation detail, for example of + the adapter. +- **The packager omits `transitional.databaseRuntime`.** Core requires no Drizzle runtime agreement. +- **The verifier checks the ABI declaration and the plain schema metadata instead.** It refuses an + ABI v1 package that carries `transitional.databaseRuntime`. Such a package was made by a packager + that did not know the module's database contract. + +A legacy module's package keeps today's `transitional.databaseRuntime` recording and validation for +as long as the legacy facade exists. + +This needs no new package format. ADR 0049 already lets a format 1 package carry no `transitional` +facts. A platform from before this change cannot misread an ABI v1 package either: it refuses the +version 8 manifest. + +### Compatibility and deprecation + +The deprecation window applies to the **legacy database facade**, not to manifest version 7. + +1. **Phase 1.** Core serves both contracts: the legacy facade to version 8 modules without a + `database` declaration, and an ABI v1 executor to modules that declare `abi: 1`. +2. **Phase 2.** The Module SDK and the module template use ABI v1. First-party modules move over + independently, each in its own change. +3. **Phase 3.** After at least one documented deprecation window, ending in a breaking Module SDK + release, the legacy facade may be removed. Its removal date, and how the manifest then expresses + the database contract, belong to that later decision. + +The legacy facade is transitional. It is not part of the permanent module contract, and it keeps +its current behaviour until it is removed. + +## Consequences + +- A module's ORM, and its version, no longer has to match Core's. Core can upgrade or replace its + own Drizzle without rebuilding any ABI v1 module. +- `transitional.databaseRuntime`, recorded in ADR 0049 as an implementation constraint, disappears + for ABI v1 packages, as ADR 0049 anticipated. +- Modules get one stable error shape with SQLSTATE, and one documented result encoding that loses + no precision and does not depend on time zones. +- A transaction object can no longer outlive its callback or reach a pooled connection afterwards. +- Every module moves to manifest version 8. Already-built version 7 packages must be rebuilt. + Database code changes only when a module opts into ABI v1. +- An ABI v1 module bundles an adapter. Its Drizzle code mostly stays as it is: the query builders + are its own. +- Core takes on a documented decoding table, transaction scoping, savepoint management, and error + mapping that Drizzle did implicitly before. Each needs live PostgreSQL test evidence. +- Core must serve two database contracts during the deprecation window. +- None of this is a sandbox. A trusted module can still do anything the API's database role can. + +## Alternatives + +- **An ORM AST or query-object ABI.** Rejected. Anything richer than SQL text and parameters is one + ORM's internal representation, so it would tie the ABI to that ORM and its release cadence. The + only part worth keeping, the row mode and field metadata, is already in the executor. +- **An Ekklesia-owned query DSL.** Rejected. It would cover less of PostgreSQL than Drizzle or + Kysely, cost more to maintain than either, and still force every module to rewrite its queries. +- **A permanent, versioned Drizzle facade.** Rejected as the permanent contract. It would make Drizzle + version agreement a permanent install-time constraint. Core's transaction object would keep + reaching module code. Core could never change its ORM. It remains only as the transitional legacy + facade. +- **Exposing the pool, a client, or a connection string.** Rejected, as in ADR 0016. Connection + lifecycle, timeouts, and transaction cleanup belong to the platform. +- **SQL parsing or execution-time prefix checks as a security boundary.** Rejected. SQL can name + tables in too many ways, and a trusted module can send any SQL, so a parser would be bypassable + and a false promise. ADR 0016 rejected the same idea. +- **`SET ROLE` or per-module PostgreSQL roles as a security boundary.** Rejected. Any SQL the module + sends can reset the role, and in-process code can reach the database without the ABI. Roles in a + module's CI, as a test aid, would be a correctness tool, not a boundary. +- **`Date` as a direct parameter.** Rejected. Its meaning depends on the target column's type, which + the ABI cannot see. Adapters convert it explicitly. +- **`pg`'s default parsing as the result contract.** Rejected. It is process-global and mutable, it + loses precision for `jsonb` numbers and `numeric[]` elements, and it is not an Ekklesia contract. +- **Parsing manifest versions 7 and 8 side by side.** Rejected. Manifest versions are hard cutovers, + and the deprecation window is about the database facade, not the manifest. + +## Deferred + +- The implementation: Core's executor, decoding table, transactions, savepoints, errors, and their + tests. +- Manifest version 8's exact mechanics, and the template and fixture cutover. +- The Module SDK's Drizzle adapter and raw-SQL helper. +- A Kysely adapter. +- The legacy facade's removal date and the release that removes it. +- Redesigning the migration file format. +- SQL cancellation and `AbortSignal`. +- Stronger process or database isolation for modules. +- Moving Signage to ABI v1. diff --git a/docs/architecture/decisions/README.md b/docs/architecture/decisions/README.md index 4fd6b13..7afc2a4 100644 --- a/docs/architecture/decisions/README.md +++ b/docs/architecture/decisions/README.md @@ -58,3 +58,4 @@ ADRs capture durable choices that affect more than one package or phase. - [0047: Every module proves itself inside Ekklesia, with one harness the platform publishes](0047-module-ci-harness.md) — Proposed - [0048: Ekklesia ships one generic image, and installed modules live outside it](0048-generic-image-and-installed-module-storage.md) — Accepted - [0049: A module release is one immutable, self-contained package: Ekklesia Module Package v1](0049-module-package-v1.md) — Accepted +- [0050: Module Database ABI v1: modules and Core exchange SQL, never ORM objects](0050-module-database-abi-v1.md) — Proposed diff --git a/docs/architecture/platform-architecture.md b/docs/architecture/platform-architecture.md index 06efa2c..6703b4f 100644 --- a/docs/architecture/platform-architecture.md +++ b/docs/architecture/platform-architecture.md @@ -686,4 +686,5 @@ Module Package v1, a deterministic `.tar.gz` with its SHA-256. and loads the backend once in a short-lived process with an empty environment. Only then does it extract anything. - **What exists today.** Only the `any` target. Drizzle agreement is recorded as transitional, not a - database ABI. + database ABI. [ADR 0050](decisions/0050-module-database-abi-v1.md), still proposed, defines the + Module Database ABI that removes it for modules that declare that ABI.