@@ -33,11 +33,22 @@ Three questions, answered from the machine:
`package-manager`, `seat`. A capability is detected and refused against; it is not something a
module can install.
3.**Write the resources in the order they must happen.** They are applied in the order written
3. **Choose each part's form, from the table in [design 20](../../03-DESIGN/01-to-be/20-writing-a-module.md) *What a
module is built of*.** Tools are a `bundle` the tool runner loads (`loads`, and every tool in `tools`); a service of
the module's own code is a `process` resource; a scheduled job says `schedule`, a one-time step `run-once`, a
migration of the module's own state `prepares`; a seat's implementation is a claim and the verbs it serves; an image
is an `image` or `upstream` artifact run by a `container`; a program of the operating system is a `package` and its
`service` resources. A bundle or a package is in Go or TypeScript, the languages the builder has build modules for.
4.**Ask for kept data in `requires`, by template** (`state`, `events`, `work-queue`, `log`, `objects`, `ledger`), each
entry with its `name`, `storage`, `template` and `options`. The old sections `state`, `logs`, `streams` and
`objects` are for the parts the bus module needs before it can answer, and for no new entry anywhere else.
5.**Write the resources in the order they must happen.** They are applied in the order written
and orphans are removed in reverse, so a `network` is written before the containers that join
it and removed after them.
4.**Put every secret in a file, never in `env`.** A declaration travels over the broker in plain
6.**Put every secret in a file, never in `env`.** A declaration travels over the broker in plain
text: a password in `env` is a password the broker sees. **The mesh delivers parts; a module
that needs them combined combines them.**
@@ -67,21 +78,39 @@ Three questions, answered from the machine:
Add `restart-on` naming the env file, or the container keeps the credential it started with
through every rotation.
5. **Pin every image by digest.** A tag moves. The manifest in a repository names artifacts; the
manifest the mesh holds names digests, and they are not the same document.
7. **Pin every image by digest.** A tag moves. An `upstream` artifact's `from`, and a container's `image` written
directly, name `@sha256:`: the builder refuses an upstream reference with a tag alone. The manifest in a repository
names artifacts; the manifest the mesh holds names digests, and they are not the same document.
6. **Decide generate or accept.** A new module's credential is generated. **An adopted one keeps
8. **Decide generate or accept.** A new module's credential is generated. **An adopted one keeps
the credential it already has** — `secret accept` — because minting a new password for a
database that already exists locks the application out of its own data.
7. **Add a provisioner only if the software cannot read a file.** A proxy that watches a
9. **Add a provisioner only if the software cannot read a file.** A proxy that watches a
directory needs nothing. PostgreSQL needs `CREATE ROLE`, an object store needs a bucket and a
policy, an identity provider needs a realm and a client — those need a small program beside
them. It reads what the mesh granted and reconciles; it does not decide anything.
8. **Prove it in the lab, against the real software.** Not that a container started — that the
thing works: the credential authenticates, a wrong one is refused, the containers reach each
other, the data survives a restart.
10. **Prove it in the lab, against the real software.** Not that a container started — that the
thing works: the credential authenticates, a wrong one is refused, the containers reach each
other, the data survives a restart.
## Where the parts of the mesh read a manifest differently
*Written 2026-11-12 from reading the manifest validator, the builder, the node-engine's vocabulary and the tool runner
on their main branches.* The manifest validator accepting a manifest does not yet mean every other part takes it.
Until each of these is closed in the part named, write the form in the right column:
| the manifest validator accepts | but | so write |
|---|---|---|
| a `bundle` or a `package` in any language word | the builder builds only Go and TypeScript bundles and TypeScript packages, and refuses the rest by name at the build | a bundle in Go or TypeScript |
| an `upstream` artifact whose `from` names a tag alone | the builder refuses an upstream reference with no `@sha256:` | the reference with its digest |
| a `log` asked for in `requires` by template | the tool runner's log calls reach only a log of the old `logs` section; a template log is reached only by its stream's read and fill calls, so a module cannot append to it through the runtime | a template log only where the module's own bus account publishes it; a log its tools append to through the runtime waits for the tool runner's fix |
| any `type` and any field on a resource | the node-engine refuses an unknown type and a field the type does not use, when the module is composed for a machine: in the change's composition step only for the machines the change touches, and for a module assigned nowhere never | each resource's fields as the node-engine's vocabulary has them (mesh-sdk's declaration package) |
| `schedule` and `run-once` on a `process` unjudged | the node-engine refuses a process with both, or a scheduled one with `restart-on`, `replaces` or `witness`, or an invalid cron | a process in one mode |
How it is checked: each row is closed by a change in the part it names, which deletes the row in the same pull
request; a reviewer of a manifest reads these rows.
## A module that calls or holds a seat, or shares code
@@ -124,8 +153,9 @@ time and the scaffolding was not.**
## Rules
- **Read the node-engine's log before theorising.** A declaration that was sent and not applied says so
there and nowhere else.
- **Read the check's verdict, then the node-engine's log, before theorising.** The change's check names every
problem the manifest validator and the composition step found; a declaration that was sent and not applied says
so in the node-engine's journal (`<node>/node-service-manager.journal`) and nowhere else.
- **A failing test is kept, not skipped.** It is the reproduction.
- **Never rotate during an adoption.** Rotation is a separate act, afterwards, deliberately.
- **A data directory is never removed by the mesh** (ADR 0030),
{n:"06",name:"Writing a module",kind:"side",c:"--side",
trig:"Something that runs today must run on the mesh.",
who:"Whoever ports or writes it.",
steps:["Read what runs: compose file, environment, where data sits.","Name what it provides and requires.","Declare capabilities for machine facts.","Write resources in the order they must happen.","Every secret in a file, never in env.","Pin every image by digest.","New: generate the credential; adopted: accept it.","Prove it against the real software: auth, refusal, restart."],
steps:["Read what runs: compose file, environment, where data sits.","Name what it provides and requires.","Declare capabilities for machine facts.","Choose each part's form (design 20).","Kept data in requires, by template.","Write resources in the order they must happen.","Every secret in a file, never in env.","Pin every image by digest, upstream too.","New: generate the credential; adopted: accept it.","Prove it against the real software: auth, refusal, restart."],
prod:"A module declaration, optionally a provisioner, and its proof.",
next:"07",bounds:"no time bound",
prev:["A service running with no password","A new password locking out adopted data","A test that cannot fail"],
- mesh-delivery modules/mesh-delivery-indexer/judge (the manifest validator)
- mesh-delivery modules/mesh-delivery-builder
- mesh-sdk go/catalogue (the manifest's shapes)
- mesh-sdk go/declaration (the node-engine's resource vocabulary)
- mesh-contracts
updated:2026-11-05
updated:2026-11-12
decisions:
- "ADR 0578"
- "ADR 0595"
- "ADR 0619"
- "ADR 0634"
- "ADR 0635"
- "ADR 0625"
- "ADR 0627"
- "ADR 0573"
- "ADR 0605"
- "ADR 0606"
- "ADR 0607"
- "ADR 0609"
- "ADR 0593"
- "ADR 0150"
- "ADR 0099"
- "ADR 0053"
- "ADR 0499"
- "ADR 0498"
- "ADR 0478"
- "ADR 0436"
- "ADR 0366"
- "ADR 0316"
- "ADR 0267"
- "ADR 0259"
- "ADR 0262"
- "ADR 0348"
- "ADR 0564"
- "ADR 0596"
- "ADR 0201"
- "ADR 0193"
- "ADR 0192"
- "ADR 0189"
- "ADR 0175"
- "ADR 0152"
- "ADR 0150"
- "ADR 0142"
- "ADR 0135"
- "ADR 0099"
- "ADR 0074"
- "ADR 0053"
- "ADR 0052"
- "ADR 0040"
- "ADR 0039"
- "ADR 0053"
- "ADR 0005"
---
# Writing a module
A worked guide. One module, four capabilities, four languages, and the packages it publishes.
A worked guide: what a module is built of, the manifest form of each part, and the packages it publishes.
The reference for *what can be said* is [`18-building-a-module`](18-building-a-module.md); the
reference for *what the code and the mesh say to each other* is
[`19-the-module-protocol`](19-the-module-protocol.md). This is how you actually write one.
The reference for *what a manifest may say* is the manifest validator, the rules of the seat `mesh-catalog`'s holder
that judge every manifest (ADR 0578 §1), with [design 32](32-what-a-module-declares.md) for what the bus makes of it;
the reference for *how a build is modelled* is [design 18](18-building-a-module.md); the reference for *what the code
and the mesh say to each other* is [design 19](19-the-module-protocol.md). This is how you actually write one.
## First: what is in the SDK, exactly
@@ -93,136 +121,144 @@ the build modules' regenerate-and-compare step, the catalog check of a client an
contracts module that claims a seat; the home of each new exported name, a domain SDK's importers included, and a role
made a seat or a seat left as one module's tools, by the review of the pull request that adds it (ADR 0634 §10).
## A module with four capabilities, in four languages
## Who reads a manifest, and what each one judges
A module is **one piece of software** (ADR 0040) and
may still be written in several languages — each artifact names its own, compiles alone and is
packed alone.
*Rewritten 2026-11-12 from the manifest validator's code on mesh-delivery's main (the operator, 2026-11-12: "first
look into the indexer's code that reads the manifest and update our module-development documentation accordingly").*
```
showcase/
module.json
events/ ← TypeScript: reacts to what the mesh does
tools/ ← Go: answers questions
provisioner/ ← Rust: grants instances of what it provides
ingest/ ← Python: a scheduled job
```
A manifest is read by four parts, and each judges only what is its own:
### The manifest
| part | reads | judges | where a refusal is said |
|---|---|---|---|
| the **manifest validator**, the rules of the seat `mesh-catalog`'s holder (the indexer) | every manifest it takes in, and every manifest a change touches, at the change's head | the manifest strictly: every field known, at every depth; every rule a manifest is held to alone (a name's form, an artifact's words, a claim on a seat of the seat list, kept data, events, secrets, health, a container's `run-once`, `schedule` and `while-stopped`) and every rule between the manifests of one catalog | the change's check, naming each problem; and the indexer's verdict, which every other part takes (ADR 0578 §1, §2) |
| the **builder** and its build modules | `build` | which build module builds each artifact (`kind` and `language`) and whether an upstream reference is pinned by digest (ADR 0478, ADR 0498) | the build's refusal, naming the artifact |
| the **node-engine**, through its composer in the change's composition step and on each node | the resources of each module composed for one node | each resource's `type` and fields strictly, by the node-engine's vocabulary (mesh-sdk's declaration package): a field a type does not use is refused, never ignored, and an `action` is refused (ADR 0005) | the composition step of the change, for each machine the change touches; the node's verdict at apply |
| every other part (the assigner, the grants part, the tool runner, the provisioner) | the manifest, decoded leniently | nothing: it takes the indexer's verdict (ADR 0578 §3) | — |
**The artifact kinds** the manifest validator accepts are five, and a builder builds each with one build module:
**Resolved from the mesh's own package registries**, which are the ordinary registries for each
ecosystem, hosted by the mesh. An author does nothing unusual: `npm install`, `go mod tidy`,
`cargo build`, `pip install` all work, and a developer's laptop resolves exactly what a build does.
| kind | its words | built by |
|---|---|---|
| `bundle` — the module's own code | `language`; for Go also `system` (one of the systems the mesh builds for), `from` (the package it compiles, relative to the module) and `binary`; for TypeScript `entrypoints` and, where it keeps imports, `external`; `loads`, `env` and `bus` for a tools bundle (below); `paths` for a language other than Go | the Go or the Node build module: a bundle in Go or TypeScript (ADR 0478); a bundle in another language is a build module of its own first |
| `package` — code other modules compile against | `language` (Go or TypeScript), `series`, `paths` (the files it is built from, ADR 0499), `generated-from` for a contracts module's | its language's build module, which asks the registry seat of its language to publish it (ADR 0605 §3, ADR 0606) |
| `image` — a container image built from a recipe | `from` (a Dockerfile), `target`, `compiles` (the Go package a recipe compiles, ADR 0267), `context` (a second repository its build reaches into) | the image build module |
| `upstream` — somebody else's image, copied | `from`, a reference pinned by its digest (`@sha256:`) | the image build module (ADR 0498 §4) |
| `archive` — files packed as they stand | `from` (a directory of the module), or an upstream release by `url`, `sha256` and `version`, with `file` or `within` (ADR 0316) | the Go build module |
### Step 2 — write each part against its capability
Every artifact may also say `needs` (an edge no other line says), and every kind but `upstream` its `series`. A `bundle` reached by nothing (no
`loads`, no `tools`, no resource naming it) is refused by name (issue 216); a resource naming a `package` is refused,
since a package is a build input and runs nowhere.
Each uses only the part of the protocol it needs. The event consumer never learns what a grant is.
**The parts, and the manifest form of each:**
```
events/index.ts on("module.builder.built", …) → the events capability
tools/main.go tool("showcase_state", …) → the tools capability
provisioner/main.rs grant → credential → the provisioning capability
ingest/ingest.py emit("module.showcase.ingested", …) → events, emitting only
```
| part | the manifest form | what runs it |
|---|---|---|
| **tools** | a `bundle` with `loads` naming what the runtime loads (for a Go bundle its `binary`), and the module's `tools` listing every tool it serves; `replaces` for the shell commands a tool replaces | the node's tool runner (ADR 0175); with `runs-as`, a **runtime of its own** under the module's account (below) |
| **a long-running service of its own code** | a `process` resource: `artifact` (a bundle), `name` (what the unit is called), `run` (the command, relative to the unpacked bundle), `user`, `env` and `env-file`, `restart-on`, `health`, `yields`, `replaces` (the ids it takes over from) | the node-engine, as a unit that stays up |
| **its own bus account for a process** | the process's `bus`: the `emits`, `state`, `queues`, `invokes` and `asks` it is granted, a subset of what the manifest declares; an own secret holding the account, owned by the process's `user` (ADR 0436 §1) | the grants part composes the account from it |
| **a scheduled job** | a `process` or `container` resource with `schedule`, a five-field cron; never with `run-once`; a process with no `restart-on`, `replaces` or `witness`; a scheduled container may name `while-stopped` (below) | the node-engine, again on each fire (ADR 0053) |
| **a one-time step** (a seed, a first-start step, a validator of a file) | a `process` or `container` resource with `run-once` true: a **run-once step**, which gates every resource after it; `restart-on` makes it run again when what it names changes (ADR 0052, ADR 0099) | the node-engine, to completion |
| **a migration of its own state before a new version runs** | `prepares` true, and a `container` or `process` that stays up running an artifact the module built: the mesh runs that program in its preparation mode (ADR 0135); never a run-once step written by hand for it | the node-engine, before the new version starts |
| **a seat's implementation** | `claims`, each `{name, scope, serves}` for a seat on the seat list (the verbs it serves; a kinded seat's `kind`); a seat the module defines for itself in `seats`; the verbs answered by its tools bundle or a process | as its tools or its process |
| **an image** | an `image` or `upstream` artifact, and a `container` resource naming it by `artifact`; or a container naming an image pinned by digest in `image` directly, for an image the module does not copy; a container stays up unless it says `run-once` or `schedule` | the node-engine, on the container runtime the module's `capabilities` name |
| **a host program from the operating system** | a `package` resource (`package`, `absent`) and a `service` resource per unit it ships (`unit`, `state`, `boot`, `scope`, `restart-on`, `reload-on`, `health`); never an `action` | the node's package manager and service manager |
| **files** | `file` and `directory` resources (a directory by `place` or `path`), an `archive` resource unpacking an `archive` artifact, template files in the module's `templates` directory | the node-engine and the provisioner client |
### Step 3 — the mesh does the rest
**A runtime of its own.** A module that says `runs-as` names an account it makes (a `user` resource of that name,
never root nor the operator's); its tools bundle is then served by a runtime of its own, as that account, on a bus
account of its own (ADR 0259 §8), whose readiness is the manifest's `runtime-health` (ADR 0366). Its secrets belong to
that account (`secrets-owner`), and its bus account's secret is the own secret `broker`. A tools bundle that holds an
account of its own instead says `bus` on its artifact, a Go bundle only, with the own secret `broker-<binary>` in a
directory the runtime's account may pass (ADR 0564).
You do not write a Dockerfile, a unit file, or a port number. The builder picks the toolchain from
the language, compiles each artifact alone, and publishes it. The controller assigns the machine
and the ports; the node-engine writes the units.
**What it keeps on the bus.** Kept data is asked for in `requires`, each entry in its long form: `name` (a local name,
lower-case letters, digits and hyphens), `storage` (`key-value`, `stream`, `object-store` or `ledger`), `template`
(`state`, `events`, `work-queue`, `log`, `objects`, `ledger`) and its `options`, or a bare stream's `configuration`
(ADR 0595 §1 to §3). The manifest validator holds each option to its bound. The **old sections**`state`, `logs`,
`streams` and `objects` are kept by the parts the bus module needs before it can answer, and by no new entry
anywhere else: those parts ask nothing by template, and any other module's use of an old section is refused once no
accepted module outside them uses that section any more (ADR 0619 §1, ADR 0595 §11). One local name is in one form.
A module reads another module's data by asking its tools or seat verbs, never by its bus state (ADR 0201, as changed
by ADR 0596).
### What it costs you to use four languages
**What it says and hears.**`emits` names its events locally; `consumes` names `<emitter>.<event>`, with `*` for one
name and `**` for the rest; `invokes` names each tool `<module>.<tool>` or verb `seat:<seat>.<verb>` it calls, or `*`,
and an entry's long form `{verb, fields}` names the fields of the answer it relies on (ADR 0152); `uses` names the
seats it sends to; `needs-approval` marks a tool, verb or event that waits for the operator's approval.
**Honestly: four sets of dependencies to keep current, and four SDKs that must agree.** The mesh
makes it possible, not free. A module in one language is simpler, and the reason to use four is
that one of them genuinely suits a part better — not that you can.
**What someone may change.** Each config value a module reads is declared once under `config` (or its alias
`settings`, never both), with its type, its default where it is a preference, and whether it is trusted; a file takes
it by `takes-config` or a template file's `${config:<key>}` (ADR 0262, ADR 0348).
*How it is checked:* the manifest validator's tests (mesh-delivery modules/mesh-delivery-indexer/judge) refuse each
form above that a manifest gets wrong, by name; the builder's plan test refuses a kind or language no build module
builds; the node-engine's declaration tests refuse a field a resource type does not use; and the showcase module in
mesh-catalog declares one part of each kind its language allows and composes on a node.
## The showcase, worked
The catalog's `showcase` module is the worked example: **one TypeScript bundle, several entrypoints, every mode**.
| part | artifact | resource | its words |
|---|---|---|---|
| the module's code | `code`, a `bundle` in TypeScript, `entrypoints` for the tools, the provisioner, the service, the run-once step and the report | — | — |
| a run-once step | `code` | `migrate`, a `process` | `run` the step's entrypoint, `run-once` true, `env-file` |
| the service | `code` | `server`, a `process` | `run` the service's entrypoint, `user` the module's account, `env-file`, `restart-on` its settings file |
| a scheduled report | `code` | `reporting`, a `process` | `run` the report's entrypoint, `schedule` a five-field cron |
| a container from somebody else's image | `helper`, an `upstream` pinned by digest | `tools`, a `container` | `network`, `volumes` mounting its broker secret, `args` |
| packed files | `files`, an `archive` from a directory | `packed`, an `archive` | `path`, `unknown` |
| its settings file | `templates`, the module's templates directory | `settings`, a `file` | `path`, `mode`, `template` |
| a seat it defines for itself | — | — | `seats`, and `claims` of it |
Nothing in it says how any part is hosted beyond its resource type — that is the mesh's: the builder picks the
toolchain from the language, compiles each artifact alone and publishes it, the assigner places the module, and the
node-engine writes the units. **A module in several languages is several bundles**, each naming its own language and
compiled alone; each language is one more build module the mesh must have, and one more set of dependencies to keep
current. The reason to use two is that one genuinely suits a part better, not that you can.
### Its code, in each language's own terms
Each part is an ordinary project in its language, depending on the mesh's packages the way it would depend on
anything, resolved from the mesh's own package registries, which are the ordinary registries for each ecosystem
hosted by the mesh (the registry seats, ADR 0605): `npm install` and `go mod tidy` work, and a developer's laptop
resolves what a build does. **A Go module requires a mesh package at the zero version only**: the builder builds
against the newest version of the package series the manifest's `packages` accepts (ADR 0573 §2, §4, ADR 0606), so no
member of a change names another's version.
Each part uses only the part of the protocol it needs: the event consumer never learns what a grant is, and a run-once
step emits only.
## Publishing a package is a capability
A module may publish libraries as well as run code. **The SDK is not special; it is simply the first
module that did this**, and its own consumers are the mesh's modules.
@@ -29,7 +29,7 @@ document is written and this one's status becomes `implemented`.
| [`17-raising-a-mesh.md`](17-raising-a-mesh.md) | How a mesh comes into existence, and how a machine joins one that exists | ADR 0067, ADR 0006, ADR 0005 |
| [`18-building-a-module.md`](18-building-a-module.md) | How a build is modelled, and why a recipe that is always a Dockerfile does not fit what a module is | ADR 0040, ADR 0039, ADR 0009 |
| [`19-the-module-protocol.md`](19-the-module-protocol.md) | What a module's code and the mesh say to each other; an SDK is an implementation of it | ADR 0074, ADR 0042, ADR 0043 |
| [`20-writing-a-module.md`](20-writing-a-module.md) | A worked guide: one module, four capabilities, four languages, and the packages it publishes | ADR 0074, ADR 0040, ADR 0039 |
| [`20-writing-a-module.md`](20-writing-a-module.md) | A worked guide: what a module is built of, the manifest form the manifest validator accepts for each part, and the packages it publishes | ADR 0578, ADR 0595, ADR 0074, ADR 0040 |
| [`21-the-installation-in-full.md`](21-the-installation-in-full.md) | Every step from a bare machine to a mesh that maintains itself, and what is not yet true | ADR 0067, ADR 0073, ADR 0014 |
| [`22-the-work-ahead.md`](22-the-work-ahead.md) | Everything decided and not yet built, in dependency order, each phase ending at a run | ADR 0074, ADR 0075, ADR 0014 |
| [`23-choosing-a-provider.md`](23-choosing-a-provider.md) | Which of several providers of a kind serves a consumer, and when a module carries its own instead | ADR 0084, ADR 0027 |
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.