The review by architectural domain found that readers of the seat list would name the indexer, never hear a removal, could deadlock a full restart, and that any folder could define a seat again; the-private-network's holder sat in mesh-sdk. ADR 0636 and the lead's rulings answer these, and the designs must say so before develop. - design 26: the seat list through a verb and two events of mesh-catalog; a reader without it acts on nothing new; a seat defined only in its domain's contracts module; states are any holder's, caller-written only for events and logs, each with its size cap, declared in the holder's requires; the renames move no state; the group's check reads every member's head; Connectivity in the mesh-networking repository, held by mesh-wireguard; acceptance tests by level and repository; two stale table rows corrected - design 47: the composer in mesh-delivery-sdk, the bus composer's seat parts in the grants part, placement's contract and check by their seat - design 20: a module's own package is fingerprinted by its own files - glossary: mesh-placement retired for tool descriptions too; the composition renamed to mesh-delivery-sdk's; six seats given their domains Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
16 KiB
layer, status, code, updated, decisions
| layer | status | code | updated | decisions | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| to-be | proposed |
|
2026-11-05 |
|
Writing a module
A worked guide. One module, four capabilities, four languages, and the packages it publishes.
The reference for what can be said is 18-building-a-module; the
reference for what the code and the mesh say to each other is
19-the-module-protocol. This is how you actually write one.
First: what is in the SDK, exactly
The protocol, and nothing else (ADR 0074). An SDK is an implementation of the module protocol in one language. If something is not in the protocol it does not belong in an SDK, and that rule is what stops it becoming the 34,000-line shared library this design exists to avoid (ADR 0039).
Split the way the protocol is split:
| the SDK gives you | so that you can |
|---|---|
| connection — reads the sealed credential, pins the certificate, takes your identity from it | reach the broker as this module on this machine, and not be able to claim otherwise |
| events — emit, subscribe, the envelope, dedup | react to what happens in the mesh |
| tools — register and serve | be asked questions |
| provisioning — receive a grant, return a credential | give a consumer an instance of what you provide |
What it does not give you, deliberately:
- No configuration loader. Configuration arrives as files the mesh wrote and environment the mesh set. Reading a file is not a thing an SDK needs to teach.
- No API clients. A Plex client belongs in the Plex module. It changes when Plex changes, which has nothing to do with any other module — frequent and cascading is the disease. Logic over a seat is not such a client: it is the seat's client package, in the seat's contracts module (below), never in the SDK and never in a holder.
- No storage, no HTTP framework, no logging library. Use the language's.
If you find yourself wanting to add something to the SDK, the test is ADR 0039's: does editing it recompile unrelated modules, and does it change often? Both, and it does not belong. It goes to its owner instead: a seat's contract or client, the module that owns it, or its domain's SDK (Where a seat's code goes, below; ADR 0634 §1).
Where a seat's code goes
Added 2026-11-04, widened 2026-11-05 (ADR 0625, ADR 0627, ADR 0634, ADR 0635; design 26 Where a seat's code goes is the whole rule). Shared code lives with its owner (ADR 0634 §1): a module that calls or holds a seat, or shares code with other modules, meets these places, and each piece of code has one, the first that fits:
| the code | lives in | written |
|---|---|---|
| a seat's types, subjects, the layouts of its states and a typed call per verb or accept | the seat's contract: a package of its own per language in its domain's contracts module (mesh-<domain>-contracts, or mesh-contracts for the Core domain and a domain with none), for a seat defined whole; a seat at the seat list's grain has only its definition, which every reader reads from the bus (ADR 0635 §1, §2) |
generated from the seat's definition, never by hand |
| logic two or more callers or holders would otherwise each write over that seat (an ask and its awaited answer, publishing a file in parts to a registry) | the seat's client package, beside its contract, owned by the contracts module | by hand; it uses only its own seat's contract, mesh-sdk's runtime and its own domain's SDK, never another seat's client, and names no holder, no module's tool and no module's subject |
| logic and state layouts one module owns, where it holds no seat for them | that module's own package, which other modules import from it: by path in the same repository (ADR 0609 §2), by a published version from another (ADR 0573 §1), so its manifest declares it as a package artifact; its fingerprint counts only that package's files (ADR 0593 §2), so its importers rebuild only when it changes | by hand, in the owner module |
| logic and forms no seat carries and no one module owns, used by one domain's modules or seats only (moving a file in parts, the build steps' shared logic) | that domain's domain SDK, mesh-<domain>-sdk, a package-only module (ADR 0634 §2) |
by hand; it imports only mesh-sdk and contract packages, names no module's tool or subject, and holds no seat's type or state layout |
| what every module needs whatever the seat (the bus connection, the tools bundle's runtime, state), and a part modules or seats of more than one domain share that names no seat, verb or seat's type | mesh-sdk | by hand; it takes no seat-specific type, imports no contracts module, and reads the seat list from the bus |
| the seat's implementation | the holder module | by hand, against the contract; never the place of the contract or a client |
So a module imports the contracts and clients of the seats it uses, the packages of the modules whose state it reads, its domain's SDK where it uses it, and mesh-sdk. Its users rebuild when something they use changes, not when any seat or any domain does. A seat you define yourself, outside the mesh's own contracts, gets a contracts module of its own, which may share your holder module's repository (ADR 0625 §7): a second holder then imports only the contract, never your holder. A contracts module and a domain SDK are modules like any other, found by their manifests in folders of a repository and delivered to no node (ADR 0634 §8). And a role no other module could ever take over is no seat at all: it is your module's own tools and events (ADR 0625 §8).
How it is checked: by the checks design 26 lists: the generators' tests, mesh-sdk's import and package-list tests, the build modules' regenerate-and-compare step, the catalog check of a client and the manifest validator's refusal of a 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
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.
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
The manifest
{
"module": "showcase",
"build": { "artifacts": [
{ "name": "events", "kind": "bundle", "language": "typescript",
"entrypoints": ["index.js"] },
{ "name": "tools", "kind": "bundle", "language": "go",
"entrypoints": ["tools"] },
{ "name": "provisioner", "kind": "bundle", "language": "rust",
"entrypoints": ["provisioner"] },
{ "name": "ingest", "kind": "bundle", "language": "python",
"entrypoints": ["ingest.py"] }
]},
"resources": [
{ "id": "events", "type": "process", "name": "showcase-events",
"artifact": "events", "run": ["node", "index.js"] },
{ "id": "tools", "type": "process", "name": "showcase-tools",
"artifact": "tools", "run": ["./tools"] },
{ "id": "grants", "type": "process", "name": "showcase-grants",
"artifact": "provisioner", "run": ["./provisioner"] },
{ "id": "ingest", "type": "process", "name": "showcase-ingest",
"artifact": "ingest", "run": ["python", "ingest.py"],
"schedule": "0 3 * * *" }
]
}
Four artifacts, four toolchains, four processes, one module. Nothing here says how any of them
is hosted — that is the mesh's, and it is why these are process rather than four containers.
Step 1 — say what it is, in each language's own terms
Each part is an ordinary project in its language, depending on the mesh SDK for that language the way it would depend on anything:
events/package.json "@novox/mesh-sdk": "^1.2.0"
tools/go.mod require novox.example/mesh-sdk v1.2.0
provisioner/Cargo.toml mesh-sdk = "1.2"
ingest/pyproject.toml dependencies = ["mesh-sdk~=1.2"]
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.
Step 2 — write each part against its capability
Each uses only the part of the protocol it needs. The event consumer never learns what a grant is.
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
Step 3 — the mesh does the rest
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 costs you to use four languages
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.
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.
{
"module": "plex",
"build": { "artifacts": [
{ "name": "server", "kind": "upstream", "from": "plexinc/pms-docker@sha256:…" },
{ "name": "client-ts", "kind": "package", "language": "typescript", "from": "clients/typescript" },
{ "name": "client-rust", "kind": "package", "language": "rust", "from": "clients/rust" },
{ "name": "client-py", "kind": "package", "language": "python", "from": "clients/python" }
]}
}
A package artifact is built and published to the mesh's registry for that ecosystem, under the
version its own project file declares. Another module then depends on it the ordinary way:
"@novox/plex-client": "^2.0.0"
Why this belongs to modules rather than being a separate thing. A client for a piece of software changes when that software changes, and the module that owns the software is the only thing that knows. Putting the Plex client anywhere else is the shared-library disease with extra steps.
What this leaves open
- Which private registry. That there is one is settled —
ADR 0014 says each module consumes its dependencies
from the private registry, the mesh's own shared library included. Which software serves it is
not: the catalog holds
verdaccio, and a git host usually serves package registries too. - And the bootstrap does not have one. ADR 0014 assumes a registry exists; on a fresh mesh nothing has installed one when the first SDK is built. That is the same pivot as everything else and it has not been designed.
- Who may publish. A builder pushing a package needs an account on that registry, which is a credential in the bootstrap path and does not exist yet.
- Versions and ranges. Everything else the mesh delivers is pinned by digest, and a range is resolved at build time from whatever the registry holds. A lock file records what was chosen, and the builder records that as the build edge — but a mesh that can rebuild a commit and get a different library is a real change from how everything else here works, and it should be a decision rather than a consequence.
A file and the thing that reads it are guarded by a gate
Written 2026-09-21, from resolving 04-ISSUES/066.
The apply is not a transaction: every resource is attempted, every failure reported, and only a
failed gate stops what follows it (ADR 0053).
So a push can half-happen, and the pairing that matters is a configuration file beside the
service that reads it. A module keeps that pair correct by declaring them in this order: the file;
a run-once container that validates it; the service, with restart-on naming the file. A file
the validator refuses reaches the disk and nothing else — the gate fails, the service after it is
left as it was, and the machine reports the push failed. The service never serves what the
validator refused. That is the grouping, made from what the manifest already has; no primitive
withholds a file from the disk, and a service that reads its file live rather than at start is the
one shape this does not protect, and should not be written.
How it is checked: the lab's coupled-pair spike declares exactly this pair, pushes a refused file, and asserts the file on disk is the new one, the service serves the old one, and the machine reports the push failed.
A run-once step may itself name what it reads under restart-on; for a step the word means
run again — a step that fetches a fact from a provider names the binding it reads, and runs
again when the provider moved. The service that consumes what the step made names the step, so it
is recreated with the new fact
(ADR 0099). How it is
checked: the node-engine's unit tests run a step again when its named file changed and not otherwise,
and recreate a container naming a step after the step ran.
A recurring step may hold its own module's containers still
Written 2026-10-02, from ADR 0189 and issue 108.
Some work cannot be done underneath a running service: an artifact store's collector rollouts the
storage and requires every writer stopped. A run-once step runs beside containers and a
scheduled one is the same container again, so until this a module had no way to say it — and the
mesh inherited a store that has never collected anything, because the predecessor said it with a
shell script and a script beside a module is not a resource in it.
A scheduled step may name while-stopped: resource ids of its own module's containers, which
the node-engine stops before the run and starts again after it, in the reverse order, whatever the step
did. Three boundaries, each refused where it can be seen earliest — its own module's containers
only, because a module that could quiesce a neighbour could stop the mesh; scheduled steps only,
because at apply the declaration is applied in order and a step already gates what follows, so a
one-time offline job says before rather than instead of; and restoring that is not conditional
on anything, because the only real risk of the field is a window that never closes.
How it is checked: the node-engine's unit tests assert stop–run–start in that order, the restart after a step that failed, the reverse order for several containers, and a service left down said loudly. The controller refuses, from the definition alone, a window with no schedule, one on a run-once step, one naming a container the module does not declare, and one naming itself.