Files
hq/03-DESIGN/01-to-be/20-writing-a-module.md
T
jschoubben bc271d4de0 A worked guide: one module, four capabilities, four languages
What the SDK contains, answered by exclusion as much as by inclusion. It is the
protocol and nothing else — no configuration loader, because configuration
arrives as files the mesh wrote; no API clients, because a Plex client changes
when Plex changes and that has nothing to do with any other module; no storage,
HTTP or logging, because the language has those. The test for anything proposed
is ADR 0039's: does editing it recompile unrelated modules, and does it change
often. Both, and it stays out.

Then the worked module: events in TypeScript, tools in Go, a provisioner in Rust,
a scheduled job in Python. Four artifacts, four toolchains, four processes, one
module — and each part is an ordinary project in its language depending on the
mesh SDK the ordinary way, so a laptop resolves what a build resolves.

And publishing a package as a module capability, which makes the SDK unspecial:
it is simply the first module that published a library. A Plex client belongs to
the Plex module because that is the only thing that knows when Plex changed.

Three things left open rather than papered over: which registry (the catalogue
holds verdaccio and a forge usually serves one too, and nothing says which is
ours), who may publish (a credential that does not exist), and what a range means
in a mesh where everything else is pinned by digest — a mesh that can rebuild a
commit and get a different library is a real change, and should be decided rather
than arrived at.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 14:12:01 +02:00

7.8 KiB

layer, status, code, updated, decisions
layer status code updated decisions
to-be proposed
mesh-catalog modules/showcase
mesh-control internal/builder
mesh-sdk src
2026-09-15
02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
02-DECISIONS/0040-what-a-module-is.md
02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md

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.
  • 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.

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 control plane assigns the machine and the ports; the host 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 registry. The catalogue holds a verdaccio module, and a forge typically serves package registries too. Two answers exist and nothing says which is the mesh's. That has to be settled before anything publishes.
  • 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.