feat: transition module packages to database ABI v1
CI / verify (pull_request) Successful in 1m1s
CI / infrastructure-smoke (pull_request) Successful in 36s
CI / migration-smoke (pull_request) Successful in 1m54s
CI / image-build (pull_request) Successful in 31s
CI / deploy-smoke (same-origin) (pull_request) Successful in 52s
CI / deploy-smoke (split) (pull_request) Successful in 1m32s

This commit is contained in:
ekklesia
2026-10-07 18:00:04 -05:00
parent 20ad1ae208
commit 70854d234c
20 changed files with 274 additions and 115 deletions
+8 -10
View File
@@ -682,13 +682,11 @@ Module Package v1, a deterministic `.tar.gz` with its SHA-256.
and `THIRD_PARTY_NOTICES`, and writes the archive.
- **The verifier.** Core's `verifyModulePackage` reads the archive in memory and refuses links,
special files, and escaping paths. It checks every digest and identity, the target and runtime,
and the transitional Drizzle agreement, runs the platform's own module and migration validation,
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. [ADR 0050](decisions/0050-module-database-abi-v1.md) defines the Module Database ABI
that removes it for modules that declare that ABI. Manifest version 8 now lets a module declare
`database: { abi: 1 }`; Core gives each such module its own attributed ABI executor and validates
its plain schema metadata. A version 8 module that omits `database` remains on the temporary
legacy facade. Packaging still uses ADR 0049's transitional Drizzle agreement in this slice; the
packager transition and template ABI migration are deliberately next.
runs the platform's own module, schema, and migration validation, 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. A manifest-v8 ABI module carries no
`transitional.databaseRuntime`; Drizzle may be bundled as its private implementation detail, and
Core validates plain schema metadata without comparing ORM versions. A version 8 module that
omits `database` remains on the temporary legacy facade and keeps ADR 0049's exact transitional
Drizzle agreement. The canonical template now demonstrates ABI v1. The legacy facade still
exists.
+9 -7
View File
@@ -603,19 +603,21 @@ Ekklesia Module Package v1 (ADR 0049) is proven at three levels, all in `pnpm ve
- `core/tests/module-package-verification.test.ts`: the verifier against hand-built archives,
including what the packager never produces. A valid package verifies and extracts with normalized
modes. A wrong digest, a corrupted or missing or unlisted file, disagreeing identities, an unknown
format, unknown transitional metadata, an incompatible target, Node.js range, SDK, or database
runtime, a missing entrypoint, a backend that imports something the package lacks, absolute and
format, unknown transitional metadata, an incompatible target, Node.js range, SDK, or legacy
database runtime, ABI metadata carrying a forbidden transitional runtime, malformed ABI schema
metadata, a missing entrypoint, a backend that imports something the package lacks, absolute and
`..` paths, symbolic and hard links, device nodes and FIFOs, setuid and setgid entries, a second
top-level directory, and a duplicate entry each fail with their own sanitized code. The backend
load check runs in a real child process.
load check runs in a real child process and selects the schema contract from the manifest.
- `packages/module-pack/tests/`: the packager against generated fixture modules (payload contents,
generated `package.json`, metadata, bundled backend with only built-in imports, refusal of native
and platform-specific dependencies and of unknown or textless licences, notices, byte-identical
archives, normalized tar and gzip headers), and end to end against the built module template:
packed twice to identical bytes without changing the workspace's install state, verified by Core's
verifier, extracted elsewhere, and loaded with Node's own resolution hooks proving nothing outside
the package is resolved. Turbo builds the template and Core first, and runs the template's own
tests (which rebuild its UI in place) before these.
verifier with a deliberately different Core Drizzle version, extracted elsewhere, and loaded with
Node's own resolution hooks proving nothing outside the package is resolved. The package contains
module-side Drizzle but no `transitional.databaseRuntime`. Turbo builds the template and Core
first, and runs the template's own tests (which rebuild its UI in place) before these.
## The generic platform image and the reference deployment
@@ -635,7 +637,7 @@ These need Docker and build images locally; nothing is pushed or published.
generic image with empty module storage. With `EKKLESIA_SMOKE_TOPOLOGY=split`, the same image also
runs the installed-module lifecycle:
- the module template is packaged by the canonical packager (Module Package v1, ADR 0049),
verified by the image's own verifier, and installed into module storage;
as ABI v1, verified by the image's own verifier, and installed into module storage;
- its migration is planned and explicitly applied, and the API restarted;
- a real browser mounts it;
- it is removed and the API restarted, and Core still runs, migrations still plan and apply, and
+16 -18
View File
@@ -138,18 +138,15 @@ The archive holds one directory named with your module id:
- **Deterministic.** The same packager version and the same built inputs produce the same archive,
byte for byte, wherever the checkout is.
**Drizzle, for now.** You define tables with your own copy of Drizzle, and Core runs your queries
through the platform's Drizzle instance (see [Database access](#database-access)). Objects from two
copies interoperate only when the copies are interchangeable, so the current implementation requires
**exactly the Drizzle version the platform uses**: the one `@ekklesia/module-sdk` depends on. The
package records it under `transitional`, and an installation refuses a package whose Drizzle differs.
This is a constraint of today's implementation, not a permanent contract.
**Database ABI v1 is the current path.** A module that declares `"database": { "abi": 1 }` keeps
Drizzle and its exact version inside its own bundle as an implementation detail. Its package omits
`transitional.databaseRuntime`; Core validates the module's plain schema metadata and never compares
the bundled Drizzle version with its own. The canonical template demonstrates this path with
`@ekklesia/module-database/drizzle`.
`@ekklesia/module-database` now contains the module-side ABI v1 adapters that remove that coupling,
but deployed modules cannot receive an ABI v1 executor yet. Manifest v8 and per-module runtime
service delivery are separate later work. Until those land, the current transitional package rule
and database service above remain authoritative; neither Module Package v1 nor the template changed
with the adapter package.
A manifest-v8 module that omits `database` remains on the temporary legacy facade. Only that legacy
package records bundled Drizzle under `transitional.databaseRuntime`, and the verifier continues to
require exact agreement with Core. The legacy facade has not been removed.
#### Verifying a package
@@ -1468,9 +1465,10 @@ export default defineConfig({
```
Keep `tables.ts` limited to Drizzle declarations so the stable generator can load it without
executing the ESM Module SDK wrapper. `src/schema/index.ts` remains the production compiled schema
entrypoint and wraps those same exported table objects with `defineModuleSchema`; tables are not
declared twice.
executing the ESM module wrapper. For an ABI-v1 module, `src/schema/index.ts` remains the production
compiled schema entrypoint and converts those same table objects to plain metadata with
`moduleDrizzleSchemaMetadata`; tables are not declared twice. A legacy-v8 module continues to wrap
them with `defineModuleSchema` while that facade exists.
Run the module's `migration:generate` script with a lowercase snake-case name, review the generated
SQL and snapshot, and add the matching entry to `drizzle/meta/_ekklesia.json`. Every entry must
@@ -1587,10 +1585,10 @@ The result is `{ moduleId, tables: [{ name, foreignKeys: [{ table }] }] }`. Core
prefixes, duplicates, and same-module foreign-key targets from those strings and never inspects an
ABI module's ORM objects.
The canonical module template has moved to manifest version 8 but intentionally omits `database`,
so it remains on the legacy facade for one more slice. The packager still emits ADR 0049's
`transitional.databaseRuntime`; the next package-transition change will remove that fact for ABI
packages and migrate the template at the same time.
The canonical module template declares ABI v1, uses the module-side Drizzle adapter, and exports
plain schema metadata derived with `moduleDrizzleSchemaMetadata`. Its Module Package v1 still
bundles Drizzle, but carries no `transitional.databaseRuntime`: that version belongs to the module,
not Core.
## Legacy database access