Files
ekklesiaandClaude Opus 5.5 98e59d4ba0
CI / verify (pull_request) Successful in 1m5s
CI / infrastructure-smoke (pull_request) Successful in 35s
CI / migration-smoke (pull_request) Successful in 1m52s
CI / package-release (pull_request) Successful in 35s
CI / image-build (pull_request) Successful in 31s
CI / deploy-smoke (same-origin) (pull_request) Successful in 45s
CI / deploy-smoke (split) (pull_request) Successful in 1m30s
CI / package-publication (pull_request) Successful in 44s
chore(release): prepare 0.2.0
Every platform workspace package and every public @ekklesia/* package
declares 0.2.0, and the deploy README's image build example names it.
templates/module keeps its own version.

0.2.0, not 0.1.3: before 1.0 a minor version is the breaking one
(docs/development/releases.md, ADR 0054), and since 0.1.2 main has
broken modules, module authors, and operators:
- #64 (ADR 0048): one generic image; installed modules live outside it;
- #66, #72 (ADR 0049, 0050): modules install as Module Package v1, and
  move to the Module Database ABI;
- #71: the manifest v8 cutover; v7 manifests are refused;
- #79 (ADR 0053): the curated SDK drops defineModuleSchema and the
  legacy ModuleDatabase type;
plus #67 (AGPL), #74, #77 (licensing, native targets), and the public
package train (#80, #81). It is also the first package train to publish:
0.1.2 never will be.

Because 0.2.0 is outside ^0.1.0, the same change moves to ^0.2.0:
- the standalone fixture's @ekklesia ranges and its exact module-pack pin;
- the compatibility ranges of the modules built for this release: the
  template, the standalone fixture, and the native proof module;
- the test fixtures that model a module of the current release, or the
  current platform's versions;
- the module-development guide's examples, and the release guide, which
  now names what a minor bump moves.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 17:26:13 -05:00
..

Ekklesia module template

This is the smallest supported Ekklesia module product. It proves the static manifest, compiled runtime and schema entrypoints, capability registration, a module-owned Drizzle table, a generated module-owned migration, logical storage-bucket declarations, and one permissioned HTTP route (GET /records, declared in register) that reads that table through @ekklesia/module-database/drizzle over the platform-provided ABI-v1 executor. Drizzle stays a module-side implementation detail; Core receives SQL and bound values, never the module's ORM objects, a raw pg client, or connection configuration. Migration execution and storage provisioning are intentionally not included.

It also ships two independently optional React roles: a primary/management UI at src/ui/index.tsx and a full-viewport display at src/display/index.tsx. One invocation of pnpm build compiles each declared role to the exact artifact in entrypoints.ui and entrypoints.display.artifact (ADR 0023). Both default-export the same role-neutral defineModuleUi({ id, version, mount }) ABI and receive the same ModuleUiHostContext; there is deliberately no mode flag. The manifest entrypoint and the host route choose the role.

Either role can be omitted independently. To remove only the display example, delete src/display/ and entrypoints.display (and remove example.display.view if nothing else uses it). To keep the module backend-only, also delete src/ui/, tsconfig.ui.json, tsconfig.ui-test.json, tests/module-ui.test.ts, the ui entrypoint, and the React/module-UI dependencies. The builder succeeds with an empty artifact list when neither role is declared.

tests/module-ui.test.ts builds both artifacts from source, validates them with parseModuleUiExport, mounts each into a real DOM, and unmounts it. Ekklesia mounts the primary UI inside its Dashboard shell and serves the display without Dashboard chrome at the stable /display/example page. See Declaring UI entrypoints.

To create a module inside this repository:

  1. Copy this directory to modules/<module-id>.
  2. Rename the package in package.json and remove private when it is ready for publication.
  3. Update the ID, name, version, namespaces, compatibility ranges, and bucket requests in ekklesia.module.json.
  4. Keep the manifest capability versions and dependency ranges aligned with the contracts the module actually implements and consumes.
  5. Replace src/server/index.ts with backend behavior only. Its defineModuleRuntime ID and version must match the manifest; do not duplicate the rest of the manifest in TypeScript. Declare HTTP routes only in register, against a permission the manifest already declares; start, stop, and every route handler — never register — receive context.services.database, a typed Module Database ABI-v1 executor. Adapt it with moduleDrizzle; do not pass ORM objects to Core.
  6. Replace the pure Drizzle declarations in src/schema/tables.ts and keep its table prefix aligned with the module ID. Keep moduleDrizzleSchemaMetadata in src/schema/index.ts aligned too; it exports plain ownership and foreign-key metadata, never ORM objects. Never import another module's schema.
  7. Update drizzle.config.ts, generate forward migrations with pnpm migration:generate --name=lowercase_name from the module directory, and review the SQL. Keep each generated tag in drizzle/meta/_ekklesia.json, classify it as none or approval-required, and change that file's moduleId when renaming the template.
  8. Run pnpm install, pnpm build, pnpm modules:validate, pnpm migrations:validate, and pnpm verify from the repository root. The declared production entrypoint is the compiled dist/server/index.js, not source TypeScript; the schema entrypoint is likewise compiled to dist/schema/index.js.

That modules/<module-id> directory is where a module is developed. An installation never runs it from there: Ekklesia's image contains no modules, and an installation installs a self-contained copy of the module into its module storage (An installed module is self-contained, ADR 0048). Copying dist/ alone is not enough, because the compiled server code imports the Module SDK and Drizzle by name. After building, package the module with the canonical packager, which bundles the backend into one deterministic Ekklesia Module Package (ADR 0049):

pnpm modules:pack --module templates/module --target any --out <directory> \
  --license-file LICENSE \
  --source-revision "$(git rev-parse HEAD)"

The module owns its feature screens and display renderer. Do not add platform navigation, the containing shell, global theme, authentication screens, or top-level routing; those belong to surfaces and Core. A module UI asks its host for relative API paths and realtime state, and never hardcodes Ekklesia routes. The display permission is enforced by the host before it serves the page or descriptor; context.permissions remains a presentation-only hint. Keep imports side-effect-free, register capabilities in register, acquire resources in start, and release them in stop. Construct Drizzle only through the module-side ABI adapter; never read DATABASE_URL or reach for a raw pg client. context.services.database is the only supported executor for this module's own tables, and it is never available during register.

The migration directory uses the pinned stable Drizzle Kit 0.31.10 PostgreSQL journal and snapshot format. Migrations are forward-only and may modify only this module's objects. Offline validation treats SQL as opaque bytes, calculates SHA-256 from the exact contents, and never opens a database or executes SQL. Destructive declarations are review metadata, not automatic SQL classification or approval.

License

The template's package.json declares "license": "AGPL-3.0-or-later", the license of Ekklesia and of official Ekklesia modules (see the repository README and LICENSE). A module started from the template begins with that declaration; an official Ekklesia module keeps it. The packaging command explicitly supplies the repository's canonical LICENSE, avoiding a duplicate file in this nested template. Its exact bytes become the package-root LICENSE, while ekklesia.package.json.license receives the source declaration. Standalone modules should keep their own license file and pass its path explicitly. The packager does not infer or download license material, and THIRD_PARTY_NOTICES remains only for bundled third-party dependencies.