Implements the standalone module-build readiness slice of ADR 0053. The public @ekklesia/* module-development packages can now be used from a module repository outside this monorepo. Nothing is published: no registry credentials, no Signage changes, and no change to the Module Package format. ADR 0053 stays Proposed.
What changes
Curated @ekklesia/module-sdk
The SDK exposes an explicit author API instead of re-exporting the @ekklesia/contracts barrel. That covers runtime definition, HTTP routes, principals, configuration, storage, realtime, and manifest/migration validation for a module's own tests.
It does not expose identity or authorization internals, Drizzle, or the legacy ModuleDatabase type. A module with no database sees services.database as never.
defineModuleRuntime now returns the SDK's own named interfaces. Before, a module that emits declarations failed with TS2883, because the inferred type reached into @ekklesia/contracts. The standalone fixture found this.
defineModuleSchema is removed. createModuleTable moves to @ekklesia/module-database/drizzle, and defineModuleSchemaMetadata is new in @ekklesia/module-database for modules that use no ORM.
Drizzle
drizzle-orm is no longer a dependency or peer of module-sdk or contracts. It stays public only as an optional peer of @ekklesia/module-database/drizzle.
@ekklesia/contracts contains no Drizzle import or declaration reference (aaa7ebf):
The legacy facade's types (ModuleDatabase, LegacyModuleRuntimeServices, AnyModuleRuntimeServices) moved to core/src/modules/database.ts, next to the facade Core builds. The facade itself is unchanged.
Contracts' runtime, route and services generics now take a plain database parameter that defaults to the ABI v1 executor. The legacy definition's database is opaque until Core specializes it.
The legacy Drizzle schema validator (ADR 0007) moved from contracts into Core: core/src/modules/legacy-schema-definition.ts, next to Core's own Drizzle dependency. Core's legacy runtime support is unchanged.
pnpm boundaries caught the reason: Core loads that validator at run time, so it cannot sit behind an optional peer in the production image.
The verifier's probe imports the validator as a .ts file, so the probe's source still runs directly under Node in tests. Core enables rewriteRelativeImportExtensions, which rewrites that import to .js in the build.
React
@ekklesia/ui takes react as a peer plus a devDependency. ADR 0023's bundling is unchanged.
runtime-imports-are-production-dependencies allows exactly this one consumer-selected peer, next to module-database's Drizzle peer. @ekklesia/ui is only ever bundled into browser artifacts; no production Node.js process loads it.
No public package imports react-dom.
Publishability
The nine public packages carry "ekklesia": { "distribution": "module-development" }. Their monorepo-root prepack scripts are removed.
typescript-config is now publishable. module-ui-host is now private.
module-pack carries its own copy of LICENSE, identical to the repository's, and a test keeps them equal. pnpm adds the workspace LICENSE only when no packed path matches /LICEN[CS]E/i, and module-pack's licenses/ directory matches.
Packager
The Node.js range comes from the module's own engines.node. A missing range fails with MODULE_PACK_RUNTIME_UNKNOWN. The packager no longer reads pnpm-workspace.yaml, and no CLI override was needed.
An Ekklesia package is recognised by an @ekklesia/ name and the distribution marker, never by its path. Registry-installed packages are therefore recorded under incorporates, still checked against moduleSdk, and never credited as third-party. A package that only borrows the scope, or only the marker, is third-party.
Release-check tooling
pnpm packages:pack (scripts/pack-public-packages.ts) removes each public package's dist, rebuilds without the Turbo cache, packs, and inspects each tarball. Allowed content:
only files entries, package.json, and LICENSE;
built output only where a source file produced it (stale output fails).
The packed manifest must have:
no workspace: range, no private field, and no install-time lifecycle script;
the distribution marker;
the coordinated version, with every installed @ekklesia dependency on that exact version;
no reference to a private package.
No file may name a local path.
pnpm test:standalone-module (scripts/test-standalone-module.sh, new CI job standalone-module) installs the packed tarballs into tests/fixtures/standalone-module, copied outside the repository, then proves it end to end (details below).
Docs
docs/module-development/README.md gains "Developing a module in its own repository". It covers:
dependencies vs devDependencies, with the exact module-pack pin;
engines.node;
allowBuilds: esbuild;
not cloning into modules/;
contracts is not a direct dependency.
It is marked "registry publication is the next slice".
The schema and legacy-database sections, repository-boundaries.md, and the README status are updated to match.
Evidence
pnpm verify passes. 2,555 tests ran, including:
new public-package boundary tests;
packager tests for engines.node, registry-style node_modules identity, the moduleSdk check, and the scope squatter;
an SDK named-type test;
Core's legacy schema validator tests.
pnpm test:migrations:integration passes: 396 tests, because Core's schema-discovery imports changed.
pnpm packages:pack packs all 9 tarballs and every inspection passes.
pnpm test:standalone-module: the fixture is an ABI v1 module with no ORM, using sql and defineModuleSchemaMetadata, plus a React UI using @ekklesia/ui and design tokens. It extends @ekklesia/typescript-config, declares engines.node, and pins module-pack exactly. From local tarballs, with no workspace link and no Ekklesia registry, it:
installs with no drizzle-orm present and exactly one react, shared by @ekklesia/ui;
typechecks with skipLibCheck: false, so every published declaration is checked, and builds, including the UI build. Against the earlier contracts tarball, the same check fails with TS2307 on drizzle-orm/node-postgres;
packs with no workspace file present;
verifies with Core's verifier;
records incorporates for all six bundled @ekklesia packages at 0.1.2;
credits no @ekklesia package and exactly one React in its notices, and leaks no local path.
Two runs from different temporary directories produced the same archive sha256.
Breaking for existing modules
A module that imports defineModuleSchema, createModuleTable, or contracts internals from @ekklesia/module-sdk, or that relies on the SDK typing the legacy Drizzle facade, must move to the APIs above. Core still runs legacy modules unchanged.
The Signage migration plan already moves it to ABI v1. Signage is untouched here.
Not in this PR
Registry provisioning and publication.
The release workflow that publishes after the tarball smoke.
Implements the standalone module-build readiness slice of [ADR 0053](docs/architecture/decisions/0053-standalone-module-development-packages.md). The public `@ekklesia/*` module-development packages can now be used from a module repository outside this monorepo. **Nothing is published**: no registry credentials, no Signage changes, and no change to the Module Package format. ADR 0053 stays **Proposed**.
## What changes
**Curated `@ekklesia/module-sdk`**
- The SDK exposes an explicit author API instead of re-exporting the `@ekklesia/contracts` barrel. That covers runtime definition, HTTP routes, principals, configuration, storage, realtime, and manifest/migration validation for a module's own tests.
- It does not expose identity or authorization internals, Drizzle, or the legacy `ModuleDatabase` type. A module with no database sees `services.database` as `never`.
- `defineModuleRuntime` now returns the SDK's own named interfaces. Before, a module that emits declarations failed with TS2883, because the inferred type reached into `@ekklesia/contracts`. The standalone fixture found this.
- `defineModuleSchema` is removed. `createModuleTable` moves to `@ekklesia/module-database/drizzle`, and `defineModuleSchemaMetadata` is new in `@ekklesia/module-database` for modules that use no ORM.
**Drizzle**
- `drizzle-orm` is no longer a dependency or peer of `module-sdk` or `contracts`. It stays public only as an optional peer of `@ekklesia/module-database/drizzle`.
- `@ekklesia/contracts` contains no Drizzle import or declaration reference (aaa7ebf):
- The legacy facade's types (`ModuleDatabase`, `LegacyModuleRuntimeServices`, `AnyModuleRuntimeServices`) moved to `core/src/modules/database.ts`, next to the facade Core builds. The facade itself is unchanged.
- Contracts' runtime, route and services generics now take a plain database parameter that defaults to the ABI v1 executor. The legacy definition's database is opaque until Core specializes it.
- The legacy Drizzle schema validator (ADR 0007) moved from `contracts` into Core: `core/src/modules/legacy-schema-definition.ts`, next to Core's own Drizzle dependency. Core's legacy runtime support is unchanged.
- `pnpm boundaries` caught the reason: Core loads that validator at run time, so it cannot sit behind an optional peer in the production image.
- The verifier's probe imports the validator as a `.ts` file, so the probe's source still runs directly under Node in tests. Core enables `rewriteRelativeImportExtensions`, which rewrites that import to `.js` in the build.
**React**
- `@ekklesia/ui` takes `react` as a peer plus a devDependency. ADR 0023's bundling is unchanged.
- `runtime-imports-are-production-dependencies` allows exactly this one consumer-selected peer, next to `module-database`'s Drizzle peer. `@ekklesia/ui` is only ever bundled into browser artifacts; no production Node.js process loads it.
- No public package imports `react-dom`.
**Publishability**
- The nine public packages carry `"ekklesia": { "distribution": "module-development" }`. Their monorepo-root `prepack` scripts are removed.
- `typescript-config` is now publishable. `module-ui-host` is now `private`.
- `module-pack` carries its own copy of `LICENSE`, identical to the repository's, and a test keeps them equal. pnpm adds the workspace LICENSE only when no packed path matches `/LICEN[CS]E/i`, and module-pack's `licenses/` directory matches.
**Packager**
- The Node.js range comes from the module's own `engines.node`. A missing range fails with `MODULE_PACK_RUNTIME_UNKNOWN`. The packager no longer reads `pnpm-workspace.yaml`, and no CLI override was needed.
- An Ekklesia package is recognised by an `@ekklesia/` name **and** the distribution marker, never by its path. Registry-installed packages are therefore recorded under `incorporates`, still checked against `moduleSdk`, and never credited as third-party. A package that only borrows the scope, or only the marker, is third-party.
**Release-check tooling**
- `pnpm packages:pack` (`scripts/pack-public-packages.ts`) removes each public package's `dist`, rebuilds without the Turbo cache, packs, and inspects each tarball. Allowed content:
- only `files` entries, `package.json`, and `LICENSE`;
- built output only where a source file produced it (stale output fails).
The packed manifest must have:
- no `workspace:` range, no `private` field, and no install-time lifecycle script;
- the distribution marker;
- the coordinated version, with every installed `@ekklesia` dependency on that exact version;
- no reference to a private package.
No file may name a local path.
- `pnpm test:standalone-module` (`scripts/test-standalone-module.sh`, new CI job `standalone-module`) installs the packed tarballs into `tests/fixtures/standalone-module`, copied outside the repository, then proves it end to end (details below).
**Docs**
- `docs/module-development/README.md` gains "Developing a module in its own repository". It covers:
- dependencies vs devDependencies, with the exact `module-pack` pin;
- `engines.node`;
- `allowBuilds: esbuild`;
- not cloning into `modules/`;
- `contracts` is not a direct dependency.
It is marked "registry publication is the next slice".
- The schema and legacy-database sections, `repository-boundaries.md`, and the README status are updated to match.
## Evidence
- `pnpm verify` passes. 2,555 tests ran, including:
- new public-package boundary tests;
- packager tests for `engines.node`, registry-style `node_modules` identity, the `moduleSdk` check, and the scope squatter;
- an SDK named-type test;
- Core's legacy schema validator tests.
- `pnpm test:migrations:integration` passes: 396 tests, because Core's schema-discovery imports changed.
- `pnpm packages:pack` packs all 9 tarballs and every inspection passes.
- `pnpm test:standalone-module`: the fixture is an ABI v1 module with no ORM, using `sql` and `defineModuleSchemaMetadata`, plus a React UI using `@ekklesia/ui` and design tokens. It extends `@ekklesia/typescript-config`, declares `engines.node`, and pins `module-pack` exactly. From local tarballs, with no workspace link and no Ekklesia registry, it:
- installs with no `drizzle-orm` present and exactly one `react`, shared by `@ekklesia/ui`;
- typechecks with `skipLibCheck: false`, so every published declaration is checked, and builds, including the UI build. Against the earlier contracts tarball, the same check fails with TS2307 on `drizzle-orm/node-postgres`;
- packs with no workspace file present;
- verifies with Core's verifier;
- records `incorporates` for all six bundled `@ekklesia` packages at 0.1.2;
- credits no `@ekklesia` package and exactly one React in its notices, and leaks no local path.
Two runs from different temporary directories produced the same archive sha256.
## Breaking for existing modules
- A module that imports `defineModuleSchema`, `createModuleTable`, or contracts internals from `@ekklesia/module-sdk`, or that relies on the SDK typing the legacy Drizzle facade, must move to the APIs above. Core still *runs* legacy modules unchanged.
- The Signage migration plan already moves it to ABI v1. Signage is untouched here.
## Not in this PR
- Registry provisioning and publication.
- The release workflow that publishes after the tarball smoke.
- Signing and provenance.
These are the reasons ADR 0053 remains Proposed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Makes the public module-development package set usable from a module
repository outside the monorepo. Nothing is published.
- Curated @ekklesia/module-sdk: an explicit author API, no contracts
barrel, no identity/authorization internals, no Drizzle and no legacy
ModuleDatabase type. defineModuleRuntime returns the SDK's own named
interfaces so a module's emitted declarations never reach into
@ekklesia/contracts. defineModuleSchema is gone; createModuleTable and
defineModuleSchemaMetadata live in @ekklesia/module-database.
- Drizzle: no longer a dependency of module-sdk or contracts. Core keeps
the legacy Drizzle schema validator (moved to core/src/modules) and
its legacy runtime support; drizzle-orm is an optional peer only.
- React is a peer (plus devDependency) of @ekklesia/ui; the boundary
checker allows exactly that consumer-selected peer.
- @ekklesia/typescript-config is publishable; module-ui-host is private.
Every public package carries "ekklesia.distribution":
"module-development" and no monorepo-root prepack.
- module-pack reads the Node.js range from the module's own engines.node
and recognizes Ekklesia packages by name plus that marker, so
registry-installed packages are incorporated, SDK-compatibility
checked, and never credited as third-party.
- scripts/pack-public-packages.ts (pnpm packages:pack) rebuilds, packs
and inspects the nine public tarballs; scripts/test-standalone-module.sh
(pnpm test:standalone-module, new CI job) installs them into a fixture
module outside the repository and proves it typechecks, builds,
packages, and verifies.
- Public-package boundary tests and module-development docs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
@ekklesia/contracts still emitted a type-only import of
drizzle-orm/node-postgres for the legacy ModuleDatabase facade, so a
standalone module type-checked only because skipLibCheck hid it.
- ModuleDatabase, LegacyModuleRuntimeServices and
AnyModuleRuntimeServices move to core/src/modules/database.ts, beside
the facade Core builds; the facade itself is unchanged.
- contracts' runtime, route, and services generics no longer name an
ORM: the database type is a plain parameter defaulting to the ABI v1
executor, and the legacy definition's database is opaque until Core
specializes it. Core and apps/api name the legacy types from Core.
- contracts drops its drizzle-orm optional peer and devDependency;
Drizzle stays public only through @ekklesia/module-database/drizzle.
- The facade's type-level test moves from contracts to Core.
- Regression proof: the standalone fixture type-checks with
skipLibCheck: false against the packed tarballs, with no drizzle-orm
installed (the smoke asserts both). Against the previous contracts
tarball the same check fails with TS2307 on drizzle-orm/node-postgres.
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.
Implements the standalone module-build readiness slice of ADR 0053. The public
@ekklesia/*module-development packages can now be used from a module repository outside this monorepo. Nothing is published: no registry credentials, no Signage changes, and no change to the Module Package format. ADR 0053 stays Proposed.What changes
Curated
@ekklesia/module-sdk@ekklesia/contractsbarrel. That covers runtime definition, HTTP routes, principals, configuration, storage, realtime, and manifest/migration validation for a module's own tests.ModuleDatabasetype. A module with no database seesservices.databaseasnever.defineModuleRuntimenow returns the SDK's own named interfaces. Before, a module that emits declarations failed with TS2883, because the inferred type reached into@ekklesia/contracts. The standalone fixture found this.defineModuleSchemais removed.createModuleTablemoves to@ekklesia/module-database/drizzle, anddefineModuleSchemaMetadatais new in@ekklesia/module-databasefor modules that use no ORM.Drizzle
drizzle-ormis no longer a dependency or peer ofmodule-sdkorcontracts. It stays public only as an optional peer of@ekklesia/module-database/drizzle.@ekklesia/contractscontains no Drizzle import or declaration reference (aaa7ebf):ModuleDatabase,LegacyModuleRuntimeServices,AnyModuleRuntimeServices) moved tocore/src/modules/database.ts, next to the facade Core builds. The facade itself is unchanged.contractsinto Core:core/src/modules/legacy-schema-definition.ts, next to Core's own Drizzle dependency. Core's legacy runtime support is unchanged.pnpm boundariescaught the reason: Core loads that validator at run time, so it cannot sit behind an optional peer in the production image..tsfile, so the probe's source still runs directly under Node in tests. Core enablesrewriteRelativeImportExtensions, which rewrites that import to.jsin the build.React
@ekklesia/uitakesreactas a peer plus a devDependency. ADR 0023's bundling is unchanged.runtime-imports-are-production-dependenciesallows exactly this one consumer-selected peer, next tomodule-database's Drizzle peer.@ekklesia/uiis only ever bundled into browser artifacts; no production Node.js process loads it.react-dom.Publishability
"ekklesia": { "distribution": "module-development" }. Their monorepo-rootprepackscripts are removed.typescript-configis now publishable.module-ui-hostis nowprivate.module-packcarries its own copy ofLICENSE, identical to the repository's, and a test keeps them equal. pnpm adds the workspace LICENSE only when no packed path matches/LICEN[CS]E/i, and module-pack'slicenses/directory matches.Packager
engines.node. A missing range fails withMODULE_PACK_RUNTIME_UNKNOWN. The packager no longer readspnpm-workspace.yaml, and no CLI override was needed.@ekklesia/name and the distribution marker, never by its path. Registry-installed packages are therefore recorded underincorporates, still checked againstmoduleSdk, and never credited as third-party. A package that only borrows the scope, or only the marker, is third-party.Release-check tooling
pnpm packages:pack(scripts/pack-public-packages.ts) removes each public package'sdist, rebuilds without the Turbo cache, packs, and inspects each tarball. Allowed content:filesentries,package.json, andLICENSE;The packed manifest must have:
workspace:range, noprivatefield, and no install-time lifecycle script;@ekklesiadependency on that exact version;No file may name a local path.
pnpm test:standalone-module(scripts/test-standalone-module.sh, new CI jobstandalone-module) installs the packed tarballs intotests/fixtures/standalone-module, copied outside the repository, then proves it end to end (details below).Docs
docs/module-development/README.mdgains "Developing a module in its own repository". It covers:module-packpin;engines.node;allowBuilds: esbuild;modules/;contractsis not a direct dependency.It is marked "registry publication is the next slice".
The schema and legacy-database sections,
repository-boundaries.md, and the README status are updated to match.Evidence
pnpm verifypasses. 2,555 tests ran, including:engines.node, registry-stylenode_modulesidentity, themoduleSdkcheck, and the scope squatter;pnpm test:migrations:integrationpasses: 396 tests, because Core's schema-discovery imports changed.pnpm packages:packpacks all 9 tarballs and every inspection passes.pnpm test:standalone-module: the fixture is an ABI v1 module with no ORM, usingsqlanddefineModuleSchemaMetadata, plus a React UI using@ekklesia/uiand design tokens. It extends@ekklesia/typescript-config, declaresengines.node, and pinsmodule-packexactly. From local tarballs, with no workspace link and no Ekklesia registry, it:drizzle-ormpresent and exactly onereact, shared by@ekklesia/ui;skipLibCheck: false, so every published declaration is checked, and builds, including the UI build. Against the earlier contracts tarball, the same check fails with TS2307 ondrizzle-orm/node-postgres;incorporatesfor all six bundled@ekklesiapackages at 0.1.2;@ekklesiapackage and exactly one React in its notices, and leaks no local path.Two runs from different temporary directories produced the same archive sha256.
Breaking for existing modules
defineModuleSchema,createModuleTable, or contracts internals from@ekklesia/module-sdk, or that relies on the SDK typing the legacy Drizzle facade, must move to the APIs above. Core still runs legacy modules unchanged.Not in this PR
These are the reasons ADR 0053 remains Proposed.
🤖 Generated with Claude Code