--- layer: as-is status: implemented code: [hal] updated: 2026-08-23 decisions: - 02-DECISIONS/0009-modules-and-the-graph.md - 02-DECISIONS/0013-schema-changes-are-numbered-migrations.md - 02-DECISIONS/0014-no-npm-workspace.md --- # Modules, manifests and features Everything the mesh installs is a module: a directory with a manifest. There is no second mechanism. ## What a manifest declares | Declares | Meaning | |---|---| | Identity | Name and version. **Version is owned by the builder** — a hand-edited version is a defect, and reviewers revert it. | | Environment | Every variable the module reads, with how each is produced: a static default, a generated secret, a value pulled from the node's own record, or a template composed from the others. A variable not declared here is invisible to the mesh and will not be generated, injected or audited. | | What it provides | The resource type this module can provision for others, and the network on which it is reachable. | | What it requires | Resources it needs from other modules, and the mapping from each resource's connection fields onto its own environment variables. | | Service shape | The primary container, and the data directories that must exist with the right ownership before it starts. | | Exposure | The public names this module's interfaces answer on, declared portably so the reverse proxy configuration can be generated rather than written. | | Images | Container images this module builds, so the pipeline builds and publishes them before publishing the module. | ## Features are the unit of work A module is not the unit the pipeline addresses. A **feature** is. A feature is a kind of content a module can carry: a service, a set of capabilities, a long-running process, managed configuration files, migrations, firewall rules, an installable application. One module can carry several. Features are **detected from directory contents**, not declared. A module with a capabilities directory has that feature; a module with a daemon directory has that one. An explicit declaration was supported and is now discouraged, because a declared list and the directory it describes drift, and the directory is the one that is true. Every pipeline command and event names a feature. There is no per-module build. ### What detection costs Detection makes the manifest shorter and the truth singular, and it makes the directory structure load-bearing in a way that is not obvious from reading a manifest. Renaming a directory changes what a module *is*, silently. The recurring failure is a hook named for a feature the module does not carry: it is skipped without complaint, and the change it was supposed to make simply never happens. An unknown key in a manifest is likewise accepted in silence — which is how a firewall rule can appear to restrict a port and restrict nothing (see [`04-ISSUES/003`](../../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md)). ## Kinds of module The kinds are not a type system — they are what the detected features add up to. - **A service module** carries a container definition. It gets a runtime directory, generated environment, data directories, and is started under supervision. - **A capability module** carries capabilities and no service. It contributes what a node can do, locally and to its peers. - **A flag module** carries nothing but a manifest. Its presence in a node's assignment is the entire content: it gates behaviour elsewhere. - **Combinations** are ordinary. A database module is a service *and* a capability provider *and* a provisioner. ## Selections A module can ship variants of the same feature and a node takes the one that fits it — a build for one accelerator or another, a configuration for a public node or a private one. The artifact stays selection-blind; the choice is a property of the assignment, held in the mesh database. This is one of the areas where behaviour has repeatedly diverged from intent, in both directions: selection files that were never packaged into the artifact at all, and a stale staged override on a node that silently won over the newly selected one. Both classes are recorded in the knowledge base; both presented as "the change did not apply" with no error. ## Dependencies between modules Modules depend on each other, above all on the shared library they all build against. There is **no workspace** ([ADR 0014](../../02-DECISIONS/0014-no-npm-workspace.md)): each module is a standalone package consuming published dependencies, including the mesh's own. The pipeline resolves modules into dependency **levels** and completes a level before starting the next, so a module always builds against its dependencies as just published. The cost is a publish-and-consume round trip for every cross-package change, and the absence of any repository-wide build. One thing that assumed a repository-wide build has stayed broken since (see [`04-ISSUES/005`](../../04-ISSUES/005-pipeline-test-harness-unbuildable/00-report.md)). ## Persistent state A module that owns state owns its migrations: numbered, written in the module's own language, compiled with it, frozen once they have run anywhere, and idempotent so that re-running is safe ([ADR 0013](../../02-DECISIONS/0013-schema-changes-are-numbered-migrations.md)). Two kinds exist and the distinction matters: migrations against the module's **own** local state, and migrations against a **provisioned** resource, which run on the node that consumes the resource rather than on the node that built the module. ## A documented rule with no enforcement Every module exposing capabilities is documented as required to declare the mesh's core runtime as a dependency. **Zero of the catalogue's modules do.** This is recorded here rather than quietly corrected, because it is the clearest instance of the rule this repository states about itself: a rule whose enforcement does not exist is indistinguishable from a wrong one, and costs more, because people believe it. Whether the rule or the catalogue is wrong has not been decided.