feat: add module contracts and discovery
CI / verify (push) Has been cancelled
CI / infrastructure-smoke (push) Has been cancelled

This commit is contained in:
ekklesia
2026-07-12 22:14:01 -05:00
parent d9489384c1
commit 57552a21a4
43 changed files with 2277 additions and 21 deletions
@@ -0,0 +1,55 @@
# 0005: Static module manifests and atomic discovery
- Status: Accepted
- Date: 2026-07-12
## Context
Ekklesia modules must be installable by placing a package in `modules/` without editing Core or a
central registry. Core must validate identity, compatibility, dependencies, capabilities, storage
requirements, and entrypoints before it runs module behavior. Importing an executable TypeScript
manifest would allow top-level module code to run before validation completed.
## Decision
Every module uses one inert, versioned `ekklesia.module.json` manifest. `@ekklesia/contracts` owns
the authoritative Zod schema and serializable types. `@ekklesia/module-sdk` re-exports those public
contracts and provides `defineModule` for typed programmatic validation without becoming a Core
service locator.
Core scans immediate non-hidden directories under the configured modules directory. Discovery
validates all manifests and package metadata, resolves entrypoints without loading them, checks
platform and Module SDK compatibility, resolves module and capability dependencies, and produces a
deterministic dependency-first order. Any error rejects the entire discovery result with sorted,
structured issues; no module server code is partially loaded.
Storage requirements are declarative. A module may request one or more logical buckets in its
manifest, but it cannot create provider clients or select physical bucket names. The later Core
storage service will provision deployment-prefixed physical buckets and give the owning module
scoped handles. Buckets remain private unless a future explicit storage contract says otherwise.
## Alternatives
- Executable `module.ts` manifests were rejected because discovery would cross the code-execution
boundary before validation.
- Embedding the full manifest in `package.json` was rejected because it couples the platform
contract to package-manager metadata and makes the manifest harder to version independently.
- A manually maintained import registry was rejected because every module installation would
require a central source edit.
- Letting modules configure SeaweedFS or S3 clients directly was rejected because Core owns
credentials, provider selection, isolation, and lifecycle.
## Consequences
JSON manifests do not provide TypeScript inference by themselves, so validation commands and clear
path-aware errors are part of the authoring workflow. A JSON Schema can be generated from the
authoritative contract later for editor completion.
Discovery currently proves entrypoint existence but does not import server behavior, execute
lifecycle hooks, provision requested buckets, or register routes, jobs, schemas, and migrations.
Those operations begin only after a complete valid discovery result in later runtime slices.
## Migration implications
The manifest includes `manifestVersion: 1`. Future incompatible shapes require an explicit schema
version and migration path rather than silently changing existing module interpretation.
+1
View File
@@ -13,3 +13,4 @@ ADRs capture durable choices that affect more than one package or phase.
- [0002: pnpm workspace and strict TypeScript](0002-pnpm-workspace-and-typescript.md) — Accepted
- [0003: Development infrastructure](0003-development-infrastructure.md) — Accepted
- [0004: Replace unmaintained MinIO baseline](0004-replace-unmaintained-minio.md) — Accepted
- [0005: Static module manifests and atomic discovery](0005-static-module-manifests.md) — Accepted
+9 -6
View File
@@ -56,11 +56,14 @@ surfaces.
- Private objects use signed access and namespaced storage paths.
- Every network service starts from least-privilege and local-only development defaults.
## Phase 1 status
## Current implementation status
Implemented now: monorepo/tooling, strict TypeScript policy, repository-boundary validation,
tests, base CI, PostgreSQL, and a development S3-compatible object store.
Implemented now: the Phase 1 repository and infrastructure foundation plus public module contracts,
the Module SDK manifest API, static versioned module manifests, automatic filesystem discovery,
compatibility/dependency ordering, structured discovery failures, and a backend-only module
template. Core can validate zero or more modules without importing their server behavior.
Target architecture, not yet implemented: Core runtime, module discovery and lifecycle, Drizzle
schema aggregation/migrations, Better Auth, permissions, storage service, Module SDK, Surface
SDK, capability/resource/action registries, API server, dashboard, Builder, and reference modules.
Target architecture, not yet implemented: lifecycle execution and runtime registration, Drizzle
schema aggregation/migrations, Better Auth, permissions, physical storage-bucket provisioning,
Surface SDK, capability/resource/action registries, API server, dashboard, Builder, and reference
feature modules.
+7 -2
View File
@@ -18,6 +18,7 @@ pnpm install --frozen-lockfile
pnpm format:check # formatting without mutation
pnpm lint # Biome recommended rules
pnpm boundaries # architectural directions, UI ownership, and cycles
pnpm modules:validate # installed module manifests, compatibility, and dependency graph
pnpm typecheck # strict TypeScript
pnpm test # Vitest
pnpm build # Turborepo package build graph
@@ -28,8 +29,12 @@ The lint, typecheck, and test commands run both repository-root checks and every
package-level Turborepo task. New packages add their own `lint`, `typecheck`, and `test` scripts;
the root verification command discovers them automatically.
CI runs `pnpm ci`, which adds Compose configuration validation to `pnpm verify`. A separate CI job
runs the live infrastructure smoke test so code-only local verification does not require Docker.
`pnpm modules:validate` scans `modules/*` without a central registry. A missing or empty modules
directory is valid and reports zero installed modules.
CI runs `pnpm ci:verify`, which adds Compose configuration validation to `pnpm verify`. A separate
CI job runs the live infrastructure smoke test so code-only local verification does not require
Docker. The explicit suffix avoids pnpm's built-in `ci` install command.
## Development infrastructure
+99
View File
@@ -0,0 +1,99 @@
# 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 contain:
```text
modules/example/
├── ekklesia.module.json
├── package.json
└── src/server/index.ts
```
`package.json` must use ESM, a strict semantic version, a unique package name, and:
```json
{
"ekklesia": {
"layer": "module"
}
}
```
Its version must match the module manifest version. Internal package dependencies use `workspace:`
ranges and supported package entrypoints.
## Manifest
The canonical manifest is inert JSON so discovery can validate a module without executing it:
```json
{
"manifestVersion": 1,
"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": {
"optional": ["media.items"]
}
},
"capabilities": ["calendar.events"],
"permissions": ["calendar.events.read"],
"storage": {
"buckets": [
{
"id": "assets",
"description": "Private event assets owned by Calendar."
}
]
},
"entrypoints": {
"server": "./src/server/index.ts"
}
}
```
Module, capability, and permission identifiers are lowercase and namespaced. Entrypoints must begin
with `./`, stay inside the package, and exist. 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 other S3 clients directly.
## Discovery behavior
Run:
```bash
pnpm modules:validate
```
Discovery checks every installed module atomically. 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 or escaping entrypoints.
Missing optional dependencies are allowed. Valid modules are ordered dependency-first with stable
identifier sorting.
Discovery does not load server code. Lifecycle execution, route/job registration, Drizzle schema
aggregation and migrations, and physical bucket provisioning are deliberately deferred.
## Start from the template
Copy `templates/module` to `modules/<your-module-id>` and follow its README. The template is part of
the workspace and is validated by the same contracts and discovery implementation as installed
modules.