Files
hq/00-META/process/06-writing-a-module.md
T
jschoubben 1ee62392b9 Playbook 06 — writing a module, from doing it once
Written after porting the first real workload end to end. Every step
exists because skipping it cost something, and the ratio is recorded
because it is the lesson: six attempts, one real bug, and the mesh was
right every time.

The rule worth carrying out of it: read the host's log before
theorising. A declaration that was sent and not applied says so there
and nowhere else — it took an hour to look, and the answer was one line.
2026-09-01 01:46:46 +02:00

4.6 KiB

Playbook 06 — Writing a module

Trigger. Something that runs today must run on the mesh, or a new capability must be declarable.

Who runs it. Whoever is porting or writing it.

Written 2026-09-01 from doing this for the first time end to end. Every step below exists because skipping it cost something.

Before anything: read what runs

A module is written from the thing, not from memory of the thing. For a port, that means its current compose file, its environment, and where its data actually sits. Assumptions about any of the three have been wrong every time they were not checked.

Three questions, answered from the machine:

why it decides something
what containers, and how do they find each other? more than one means a network; names between them must match what the software is configured to dial
where is its data? a bind mount moves with a path; a named volume does not; an anonymous volume is already losing data on every redeploy
which values are secret, and which are merely settings? a secret goes in own-secrets or a grant; a setting goes in the manifest and may be overridden per node

The steps

  1. Name what it provides and requires, if anything. A name is what a consumer is coupled to, not the role it plays (ADR 0027): postgres-database, not database. Most modules provide nothing and require nothing — an application is usually a leaf.

  2. Declare capabilities, not dependencies, for facts about the machine. container-runtime, 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 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 text: a password in env is a password the broker sees. Use env-file for containers, or ${secret:name} inside a config file the module supplies. The mesh delivers parts; a module that needs them combined combines them.

  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.

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

What the first port actually cost

Six attempts, one real bug. Recorded because the ratio is the lesson: the mesh was right every time and the scaffolding was not.

  • A shape existed in the language and no host implemented it, so every declaration carrying one was refused whole — correctly, and the host said exactly that. Nobody was reading the host's log. Read it first; it is the only place that says why a machine did nothing.
  • A blind find-and-replace renamed a provision in quotes and missed the same word bare.
  • A command was tested only for the invocations that should fail, so it rejected every real one and the suite stayed green.
  • A test asserted on a helper rather than on the code that calls it, three separate times. A test that cannot fail when the behaviour is deleted is not defending the behaviour.

Rules

  • Read the host's log before theorising. A declaration that was sent and not applied says so there 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), and that protects against the mesh only — not against a disk or a mistaken command.