feat: one generic image, with installed modules in installation-owned storage (ADR 0048)
CI / verify (pull_request) Failing after 47s
CI / infrastructure-smoke (pull_request) Skipped
CI / migration-smoke (pull_request) Skipped
CI / image-build (pull_request) Skipped
CI / deploy-smoke (same-origin) (pull_request) Skipped
CI / deploy-smoke (split) (pull_request) Skipped
CI / verify (pull_request) Failing after 47s
CI / infrastructure-smoke (pull_request) Skipped
CI / migration-smoke (pull_request) Skipped
CI / image-build (pull_request) Skipped
CI / deploy-smoke (same-origin) (pull_request) Skipped
CI / deploy-smoke (split) (pull_request) Skipped
The production image now contains zero feature modules and is the same for every installation. Installed modules live in module storage that one Core resolver selects from EKKLESIA_MODULES_DIR: the checkout's modules/ when unset, an absolute path that must exist when set (/var/lib/ekklesia/modules in the image). A relative, blank, missing, or unreadable configured directory fails closed with a sanitized error that never contains the value. - Host configuration reads EKKLESIA_MODULES_DIR. The API, migration plan, apply and validate, module validation, module enablement, and Doctor all resolve it the same way; the test-only API override still takes precedence. - Doctor receives Core's migrations directory explicitly instead of deriving it from the modules directory's parent. - The migration runner retains and reports the applied history of a module that is not installed, and plans and applies Core's and present modules' migrations around it. Core's history and every installed module's history stay strict: a missing or changed applied migration, or a downgrade, still fails. No down migrations, no data deletion. - deploy/Dockerfile copies no module and installs nothing for the module template; .dockerignore excludes modules/ and the build refuses a context that contains it; the runtime has an empty root-owned /var/lib/ekklesia/modules and sets EKKLESIA_MODULES_DIR. - deploy/compose.yaml bind-mounts EKKLESIA_MODULES_HOST_DIR read-only into api and migrate, with create_host_path disabled. - scripts/check-image.sh proves the image holds no modules and that a populated modules/ directory leaves its runtime file system identical; CI runs it. - The split deploy smoke now uses the same generic image: it boots with no modules, installs a self-contained test module packaged from the template (test-only), migrates and restarts, removes it (Core runs, planning reports the retained history, data survives), and restores it with its data. Docs: deploy README (module storage, install/update/remove/restore, the runtime modules target), .env.example, API configuration, module development guide, testing, workflow, security, and the architecture overview. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4d28a82937
commit
e36b70d8d4
@@ -26,7 +26,10 @@ application's. A module consumes those; it never redefines or bypasses them.
|
||||
|
||||
## Required package shape
|
||||
|
||||
Each immediate child of `modules/` must be an ESM workspace package with a unique package name, a
|
||||
A module is developed as an ESM workspace package in a checkout's `modules/` directory, and it is
|
||||
installed, self-contained, into an installation's module storage
|
||||
([An installed module is self-contained](#an-installed-module-is-self-contained)). In a checkout,
|
||||
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
|
||||
@@ -66,6 +69,72 @@ module with no migration history omits `entrypoints.migrations` and needs no pla
|
||||
The package version must match the manifest and runtime-definition versions. Internal package
|
||||
dependencies use `workspace:` ranges and supported public package entrypoints.
|
||||
|
||||
### An installed module is self-contained
|
||||
|
||||
Ekklesia's image contains no modules
|
||||
([ADR 0048](../architecture/decisions/0048-generic-image-and-installed-module-storage.md)). An
|
||||
installation keeps installed modules in its own module storage, the directory `EKKLESIA_MODULES_DIR`
|
||||
names (`/var/lib/ekklesia/modules` in the production image, the checkout's `modules/` when it is
|
||||
unset). Discovery reads it only when a process starts. Nothing is hot loaded, and an installed module
|
||||
is read-only to the platform.
|
||||
|
||||
An installed module directory is **self-contained for runtime use**. Everything the platform reads
|
||||
or loads from it, and everything those files import at run time, resolves inside it:
|
||||
|
||||
- `package.json`, with no `workspace:` range needed at run time, and `ekklesia.module.json`;
|
||||
- every built artifact the manifest declares: `dist/server`, `dist/schema`, jobs, and the UI,
|
||||
display, and contribution bundles;
|
||||
- the `drizzle/` migrations directory, with its journal and `_ekklesia.json`;
|
||||
- a `node_modules` holding the production dependencies of your server, schema, and jobs code,
|
||||
including your own copies of `@ekklesia/module-sdk`, `@ekklesia/contracts`, and `drizzle-orm`.
|
||||
|
||||
**Copying `dist/` is not an installed module.** Your compiled server code imports
|
||||
`@ekklesia/module-sdk` and `drizzle-orm` by name, and Node resolves those only from your module's
|
||||
own directory upwards. The platform's `node_modules` is never on that path, and Ekklesia adds it
|
||||
through no link, `NODE_PATH`, loader hook, or alias. A module whose imports do not resolve stops the
|
||||
API from starting with a sanitized `MODULE_RUNTIME_IMPORT_FAILED`, so test the directory you ship,
|
||||
not your workspace.
|
||||
|
||||
**Native dependencies** must match the platform image's runtime: Node.js 24.18.0 on Debian 13
|
||||
(glibc), on the image's CPU architecture.
|
||||
|
||||
**Drizzle, for now.** You define tables with your own copy of Drizzle, and Core runs your queries
|
||||
through the platform's Drizzle instance (see [Database access](#database-access)). Objects from two
|
||||
copies interoperate only when the copies are interchangeable, so the current implementation requires
|
||||
**exactly the Drizzle version the platform uses**: the one `@ekklesia/module-sdk` depends on. This is
|
||||
a constraint of today's implementation, not a permanent contract.
|
||||
|
||||
Browser artifacts are already self-contained bundles, so nothing changes for them.
|
||||
|
||||
From a workspace, `pnpm --filter <your package> deploy --prod --legacy <directory>` produces such a
|
||||
directory; remove the self-reference pnpm leaves at
|
||||
`node_modules/.pnpm/node_modules/<your package>`, which points back into the workspace. The
|
||||
repository's own smoke test packages the module template this way
|
||||
(`scripts/lib/test-module-package.Dockerfile`). That is a test fixture, not a package format: the
|
||||
format modules are delivered in, how they are signed, and how they reach an installation are not
|
||||
decided yet.
|
||||
|
||||
The repository's boundary checks (`pnpm boundaries`) prove only source modules in a workspace. An
|
||||
installed package normally has no source, so the platform cannot check it the same way. Your
|
||||
module's own CI is where its boundary rules, its production dependencies, and its behaviour inside
|
||||
Ekklesia are proven ([ADR 0047](../architecture/decisions/0047-module-ci-harness.md)).
|
||||
|
||||
### Removing a module keeps its data
|
||||
|
||||
Removing your module's directory from module storage is not deleting its data:
|
||||
|
||||
- your tables and rows, buckets and objects stay;
|
||||
- the platform keeps its enablement decision, your configuration, its Dashboard placement, and
|
||||
permission grants;
|
||||
- your applied migration history stays, and the migration plan reports it as retained history for
|
||||
a module that is not installed.
|
||||
|
||||
Reinstalled with the same module id, your module finds all of it again. Its history is validated
|
||||
strictly again, so a reinstalled package must still contain every migration that was ever applied,
|
||||
unchanged. Never delete or edit a released migration, and never ship an older version over newer
|
||||
data. Disabling is different from removing: a disabled module stays installed and is simply not
|
||||
loaded. Deleting a module's data (purge) does not exist yet.
|
||||
|
||||
## Canonical manifest
|
||||
|
||||
`ekklesia.module.json` is inert and authoritative, so Core can validate every installed module
|
||||
@@ -1447,7 +1516,8 @@ pnpm migrations:plan
|
||||
pnpm verify
|
||||
```
|
||||
|
||||
`pnpm modules:validate` builds the workspace and then atomically validates every installed module.
|
||||
`pnpm modules:validate` builds the workspace and then atomically validates every installed module,
|
||||
from the checkout's `modules/` or from `EKKLESIA_MODULES_DIR` when it is set.
|
||||
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 or schema entrypoints.
|
||||
|
||||
Reference in New Issue
Block a user