Files
Ekklesia/docs/module-development/README.md
T
ekklesia 7b448d8706
CI / verify (push) Successful in 2m14s
CI / infrastructure-smoke (push) Failing after 1m36s
feat: add module runtime lifecycle
2026-07-12 22:49:51 -05:00

157 lines
5.0 KiB
Markdown

# Module development
An Ekklesia module is a backend capability package. It owns business behavior and data declarations;
it never owns components, styles, layouts, renderers, navigation, or other UI.
## Required package shape
Each immediate child of `modules/` must be an ESM workspace package with a unique package name, a
strict semantic version, and the module layer marker:
```text
modules/example/
├── dist/server/index.js
├── ekklesia.module.json
├── package.json
└── src/server/index.ts
```
```json
{
"type": "module",
"ekklesia": {
"layer": "module"
}
}
```
The package version must match the manifest and runtime-definition versions. Internal package
dependencies use `workspace:` ranges and supported public package entrypoints.
## Canonical manifest
`ekklesia.module.json` is inert and authoritative, so Core can validate every installed module
without executing module code:
```json
{
"manifestVersion": 2,
"id": "calendar",
"name": "Calendar",
"version": "0.1.0",
"compatibility": {
"platform": "^0.1.0",
"moduleSdk": "^0.1.0"
},
"dependencies": {
"modules": {
"required": {
"people": "^0.1.0"
}
},
"capabilities": {
"required": {
"people.records": "^2.0.0"
},
"optional": {
"media.items": ">=1.1.0 <2.0.0"
}
}
},
"capabilities": [
{
"id": "calendar.events",
"version": "1.0.0"
}
],
"permissions": ["calendar.events.read"],
"storage": {
"buckets": [
{
"id": "assets",
"description": "Private event assets owned by Calendar."
}
]
},
"entrypoints": {
"server": "./dist/server/index.js"
}
}
```
Module, capability, and permission identifiers are lowercase and namespaced. A provided capability
has its own exact semantic version because its public contract can evolve independently from the
module release. Capability dependencies use semantic-version ranges. Optional means the provider
may be absent; if it is installed, its capability version must be compatible.
Server entrypoints must be compiled ESM `.js` or `.mjs` files, begin with `./`, remain inside the
package, and exist. Production never imports source TypeScript directly. Unknown manifest
properties are rejected, including presentation fields such as components or renderers.
Storage bucket IDs are logical names scoped to the module. Modules may request multiple buckets.
Core will later map them to deployment-specific physical S3 buckets and provide scoped access;
module code must not configure SeaweedFS or another S3 client directly.
## Runtime server definition
The source entrypoint exports one typed runtime definition and does not repeat the static manifest:
```ts
import { defineModuleRuntime } from "@ekklesia/module-sdk";
export default defineModuleRuntime({
id: "calendar",
version: "0.1.0",
register({ capabilities }) {
capabilities.provide("calendar.events", calendarEvents);
},
async start({ capabilities }) {
const people = capabilities.resolve("people.records");
// Acquire runtime resources here.
},
async stop() {
// Release runtime resources here.
}
});
```
ID and version are intentional build-artifact identity markers and must match the manifest. Core
rejects missing, duplicate, or undeclared capability registrations. A module may resolve only its
own capabilities and the capability dependencies declared in its manifest. The runtime context is
deliberately limited to the module ID and capability access.
Entrypoint imports must have no top-level side effects. Modules are trusted backend plugins, and ESM
evaluation cannot be rolled back. Keep registration declarative; open resources in `start` and
close them in `stop`.
## Discovery and lifecycle
From the repository root, run:
```bash
pnpm install
pnpm modules:validate
pnpm verify
```
`pnpm modules:validate` builds the workspace and then atomically validates every installed module.
It rejects malformed manifests, duplicate IDs or package names, version mismatches, incompatible
platform or SDK ranges, missing required modules or capabilities, incompatible installed
dependencies, cycles, and missing, escaping, or non-JavaScript server entrypoints. Missing optional
dependencies are allowed. Valid modules are ordered dependency-first with stable identifier sorting.
Core completes discovery before importing any server entrypoint. It then loads and verifies every
runtime definition before running hooks, registers all modules dependency-first into an unpublished
staged capability registry, and starts them in the same order. Shutdown is reverse-order. If a later
module fails to start, Core stops already-started modules in reverse order and reports any cleanup
failures without leaking caught exceptions or machine paths. A failing `start` hook must clean up
its own partial work before rejecting.
## Start from the template
Copy `templates/module` to `modules/<your-module-id>` and follow its README. No central registry edit
is needed: the workspace glob, build, discovery, and runtime loading all find the new package.