Stage 1 PR 7 (ADR 0063). A module becomes a first-class part of the Dashboard. - Manifest v9 = v8 plus optional `navigation`: a Lucide icon from the curated MODULE_NAVIGATION_ICONS set, and up to 12 primary pages (stable id, safe label, kebab-case path relative to the module root; no traversal, schemes, queries, duplicates, or nesting). Validated before activation. - Version 8 stays accepted beside version 9, each read strictly as itself (discriminated union); no released manifest is rewritten. The template moves to v9; the released Signage 0.1.5 manifest is a test fixture. - Real module routes: /<id> is the landing view, /<id>/<page> a declared page, anything beneath a page a module-owned detail route; anything else is not-found and mounts nothing. api/assets/display are never pages. - Sidebar: declared icons, pages nested under an open module, one aria-current, collapsed and drawer behaviour; v8 modules unchanged. - module-ui-sdk: ModuleUiHostContext.navigation (location, subscribe, navigate, link, settings), refusing anything outside the module's routes; undefined on the display host. Mount ABI unchanged. - Return-to-origin: one `?return=` carrier on Settings URLs, validated by parseReturnTarget and against running modules' declared routes; Settings Back returns to exactly the originating module page, across refreshes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
103 lines
7.2 KiB
Markdown
103 lines
7.2 KiB
Markdown
# 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](../../docs/architecture/decisions/0023-module-ui-build-pipeline.md)). 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](../../docs/module-development/README.md#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](../../docs/module-development/README.md#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):
|
|
|
|
```bash
|
|
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. Its manifest (version 9) declares its
|
|
sidebar icon and two primary pages, Records and About. Its UI renders whichever page
|
|
`context.navigation.location()` names, links with `navigation.link()`, shows a record's detail at
|
|
`records/<id>` beneath the Records page, and opens its Settings page with `navigation.settings`
|
|
([ADR 0063](../../docs/architecture/decisions/0063-module-navigation-contract-manifest-v9.md)). Do
|
|
not add a tab bar for those pages, the containing shell, global theme, authentication screens, or
|
|
top-level routing, and never touch `window.history`; 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](../../README.md#license) and
|
|
[LICENSE](../../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.
|