The architecture 0117 opened needs a module to offer a service as a role on the bus — one holder, addressed by what it does. A closed table in the controller cannot express that: a capability a module contributes would require changing the mesh itself. But 0110 closed the set for a good reason — nothing could say what seats a mesh had, and the hand count came out at eleven of thirteen. That argues for enumerable, not hardcoded, and 0110 weighed free-form against a fixed table without considering a third option: closed at any moment and derived from the catalogue. A derived list cannot drift, which is how the count broke. So: the mesh's seats stay the mesh's, reserved by the mesh- prefix so the prefix is the rule and there is no list to maintain; ten seats are renamed to restore 0079's convention; everything 0110 decided about what a seat IS survives untouched. Design 29 carries the declaration model: three namespaces, subjects derived from local names so a manifest survives the wire changing, queues never declared, five relationships (the job and state shapes 0041 had no room for), and the build-publish-deploy lifecycle with hard, soft and build-time dependencies distinguished. 0041 gets a progressive insight: "no per-consumer setup, only a subscription" was a fact about a topic exchange, and a JetStream durable consumer is a real object someone creates. WBS 1.3/1.4 were wrong and say so: streams come at registration and consumers at assignment, so only the foundation set belongs at genesis.
216 lines
12 KiB
Markdown
216 lines
12 KiB
Markdown
---
|
||
layer: to-be
|
||
status: proposed
|
||
code: []
|
||
updated: 2026-09-26
|
||
decisions:
|
||
- 02-DECISIONS/0118-a-module-declares-its-own-seats.md
|
||
- 02-DECISIONS/0117-the-bus-is-the-only-broker.md
|
||
- 02-DECISIONS/0106-the-bus-is-nats.md
|
||
- 02-DECISIONS/0041-events-are-a-relationship.md
|
||
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||
- 02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md
|
||
---
|
||
|
||
# 29. What a module declares, and what the bus makes of it
|
||
|
||
**The bus is ambient.** No module requires it, the way no module requires a filesystem. Every
|
||
module gets a connection and an identity whether it asks or not. What a module declares are
|
||
*relationships*; subjects, streams, consumers and permissions are all derived from those, and a
|
||
manifest never contains one.
|
||
|
||
This document is the declaration model. [Design 25](25-the-bus-on-nats.md) is the bus itself —
|
||
subjects, streams, accounts, enrolment — and stays the authority on the wire.
|
||
[Design 19](19-the-module-protocol.md) is the specification an SDK implements, and is rewritten
|
||
onto this in step 3 of [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md).
|
||
|
||
## 1. A module names locally; the mesh derives the subject
|
||
|
||
This is the load-bearing rule.
|
||
[ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) says a module
|
||
definition names no node, mesh or path. A transport address is the same class of thing: if
|
||
manifests held literal subjects, reorganising the subject space would mean editing every module in
|
||
the catalogue, and the mesh would have hundreds of copies of a decision it made once.
|
||
|
||
| declared | derived |
|
||
|---|---|
|
||
| `emits: order.placed` | publish on `mesh.mod.<module>.order.placed` |
|
||
| `consumes: billing.order.placed` | durable consumer on `mesh.mod.billing.order.placed` |
|
||
| `serves: status` | queue-group subscription on `mesh.mod.<module>.tool.status` |
|
||
| seat `telegram-sender`, `accepts: send` | work-queue consumer on `mesh.seat.telegram-sender.send` |
|
||
| `uses: telegram-sender` | publish on that seat's `accepts` subjects, and nothing else |
|
||
|
||
**The test this must pass: the manifest survives the wire changing.** Reorganise the subject space
|
||
and every manifest in the catalogue is still correct. That is the property
|
||
[ADR 0039](../../02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md) gave the sdk, applied to
|
||
declarations.
|
||
|
||
## 2. Three namespaces, and nothing else
|
||
|
||
**Its own** — `mesh.mod.<module>.>`. Its events and its tools. Nothing else may publish into it,
|
||
so an event's source is a fact the bus enforces rather than a claim in the body.
|
||
|
||
**Seats it holds** — `mesh.seat.<seat>.>`. Full participation: consume what the seat accepts,
|
||
publish what it emits, serve what it serves.
|
||
|
||
**Seats it uses** — publish only, and only on the `accepts` half. A sender cannot subscribe to a
|
||
seat's inbound subject and watch other modules' traffic, and cannot publish the seat's outbound
|
||
events and lie about outcomes.
|
||
|
||
A module naming anything outside these three is refused at registration. The whole permission set
|
||
is derivable from the declaration; nobody writes an access rule.
|
||
|
||
## 3. Queues are derived, never declared
|
||
|
||
A module says what it reacts to, not how delivery works. Each `consumes` becomes one durable
|
||
consumer; a seat's `accepts` becomes one work-queue consumer with a queue group named for the
|
||
seat. The module does not name them, does not know their names, and cannot misconfigure them —
|
||
and the controller stays the only writer of stream and consumer definitions
|
||
([design 25](25-the-bus-on-nats.md) §3).
|
||
|
||
**Retention belongs to whoever owns the namespace, not to a consumer.** A seat declares how long
|
||
its inbound backlog survives, because that is a property of the service:
|
||
|
||
```
|
||
seat: telegram-sender
|
||
scope: mesh
|
||
accepts: send retain 7d
|
||
emits: delivered, failed
|
||
serves: status
|
||
```
|
||
|
||
If each consumer could tune it, the mesh's durability would be an emergent property of whichever
|
||
manifest was edited last.
|
||
|
||
**Scope gives per-node workers without a new concept.** A module running on three nodes that each
|
||
need their own queue declares a node-scoped seat: one holder per node, three queues, same
|
||
machinery. Mesh-scoped and node-scoped seats already exist; here they do the work of "one shared
|
||
service" versus "one worker per machine".
|
||
|
||
## 4. Five relationships
|
||
|
||
| | provision | event | job | state | tool |
|
||
|---|---|---|---|---|---|
|
||
| shape | 1:1 resource | 1:many | N:1 | 1:1 | 1:1 |
|
||
| addressed to | a provider | the emitter's own namespace | a **seat** | one node | a module or seat |
|
||
| who must act | the provider | nobody | exactly one holder | that node | the server |
|
||
| credential | sealed, per consumer | none | none | none | none |
|
||
| reply | — | none | none, or an event later | a report | awaited |
|
||
| retention | — | age and size | work queue, explicit ack | **last per subject** | none |
|
||
| declared | `provides`/`requires` | `emits`/`consumes` | seat `accepts` / `uses` | the mesh's own | `serves` |
|
||
|
||
**Job** is the one [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) had no room
|
||
for. Its table has events at 1:many and provisions at 1:1; a module submitting work to a service
|
||
is neither. It is not an event, because an event is a broadcast nobody is obliged to act on and a
|
||
second holder would do the work twice. It is not a provision, because there is no resource and no
|
||
credential. What makes it safe is not cleverness in the subscribe call but the seat: exactly one
|
||
holder, so exactly one worker, by construction.
|
||
|
||
**State** is the shape the deploy path needs and nothing else uses. A declaration is not an event
|
||
— replaying yesterday's is actively harmful — and not a job. Only the newest matters, which is
|
||
last-per-subject retention, and a node that has seen sequence *n* refuses *n−1* by construction.
|
||
That is the wire-level answer to
|
||
[issue 107](../../04-ISSUES/107-a-declaration-carries-no-order/00-report.md).
|
||
|
||
## 5. Seats
|
||
|
||
A module declares a seat with its protocol, and the mesh enforces one holder at its scope
|
||
([ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md)). A caller declares that
|
||
it uses the *seat*, never the module, so the implementation can be replaced under it.
|
||
|
||
- The set of seats is **derived** — the mesh's own, plus every registered module's — so it is both
|
||
closed and extensible, and enumerating it is a query rather than an inventory.
|
||
- `mesh-*` is **reserved**: the prefix is the reservation rule, and a module declaring one is
|
||
refused at registration.
|
||
- Two modules declaring the same name: the second is refused.
|
||
- A module may not claim a seat whose protocol it does not implement.
|
||
- **Nobody holding a seat is not an error.** The stream exists from registration, so work queues
|
||
until a holder appears. Install the telegram module a week later and the backlog flushes.
|
||
|
||
## 6. The lifecycle: build, publish, deploy
|
||
|
||
Every shape above appears once, in order, and no step knows where the next one runs.
|
||
|
||
**A change lands.** The module holding `mesh-git` emits `pushed` — repository, ref, commit. An
|
||
event, because it is a fact about git and git's identity is the meaning.
|
||
|
||
**The change becomes work.** The controller consumes `pushed`, asks the catalogue which modules
|
||
are built from that repository and path
|
||
([ADR 0069](../../02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md)), and submits one
|
||
**job** per affected module to the `mesh-build-machine` seat. The builder stays simple: it builds
|
||
what it is handed, and never resolves anything. A builder that dies mid-build has its job
|
||
redelivered, because a work queue with explicit ack is what that means.
|
||
|
||
**The artifact is published.** The builder pushes to the registry seats and emits `built` —
|
||
module, version, digest. An event again: a fact about the builder.
|
||
|
||
**The build cascade is that event fanning out through a graph the mesh already has.** A module
|
||
whose image is built *on* another's artifact declares that in `build.on`. So `built` reaches the
|
||
controller, which walks the declared graph and submits rebuild jobs for everything downstream. A
|
||
dependency cascade is not special machinery — it is one event, one derived graph, and the same job
|
||
queue.
|
||
|
||
**Deployment is state, not a message.** The controller composes each affected node's declaration
|
||
and publishes it last-per-subject. A node that was away gets exactly the current one, never a
|
||
queue of superseded ones, and a replayed older one is refused by sequence.
|
||
|
||
**Applying is reported to a role.** The host applies and reports to the `mesh-controller` seat —
|
||
not to an address it was given at genesis. Held and retried while the store restarts
|
||
([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)).
|
||
|
||
What disappears across that chain is every address. No webhook URL, no registered callback, no
|
||
"which node is the builder on", no controller endpoint baked into a joining node. That is the
|
||
class of bug
|
||
[issue 102](../../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md)
|
||
names, dissolved rather than fixed.
|
||
|
||
## 7. Modules depending on each other
|
||
|
||
Three kinds, and conflating them is how deployment ordering goes wrong.
|
||
|
||
**Build-time** — A's image is built on B's artifact. Resolved by the cascade above; nothing at
|
||
runtime cares.
|
||
|
||
**Provision** — A requires a database from B. A **hard** dependency: the credential must exist
|
||
before A can start, so resolution gates delivery and A is shown as waiting until B has answered
|
||
([design 27](27-a-module-requires-the-mesh-resolves.md)).
|
||
|
||
**Seat** — A uses B's seat. A **soft** dependency, and this is the one the bus changes. A starts
|
||
whether or not anyone holds the seat, because the stream absorbs the gap. Deployment order stops
|
||
mattering for everything expressed this way, and a service being restarted, moved or upgraded is
|
||
not an outage for its callers — it is latency.
|
||
|
||
That difference is worth choosing on purpose. A dependency expressed as a provision must be
|
||
ordered; the same dependency expressed as a seat need not be.
|
||
|
||
## 8. Open
|
||
|
||
**Protocol versioning.** A seat's protocol is a compatibility surface between modules that do not
|
||
know each other, and nothing here says what happens when it changes under callers already bound to
|
||
it. This is the first thing to answer and the one most likely to hurt in year two rather than week
|
||
one.
|
||
|
||
**Whether a module may declare a seat it does not itself claim** — the contract as one thing, the
|
||
implementation as another, which is how two competing implementations would ever exist.
|
||
|
||
**Whether `consumes` naming another module couples too tightly.** It is kept here deliberately —
|
||
an event's provenance is its meaning — but a consumer of `billing.order.placed` does depend on
|
||
billing existing under that name.
|
||
|
||
## 9. How it is checked
|
||
|
||
- **A manifest holds no subject.** A catalogue test: no manifest contains a string matching the
|
||
subject grammar. The rule is worthless if it is followed by convention.
|
||
- **Permissions are exactly the three namespaces.** A composition test per module: the derived
|
||
permission set equals what its declaration implies, and a hand-written addition to it fails.
|
||
- **A sender cannot read the queue it writes to.** A bed: a module declaring `uses` is refused
|
||
subscribe on that seat's inbound subject.
|
||
- **One holder, one delivery.** A bed: a seat's job delivered once with the holder running, and
|
||
a second claim of the seat refused.
|
||
- **A queued job survives no holder.** A bed: submit with the seat unheld, assign the holder,
|
||
the job is delivered.
|
||
- **The cascade rebuilds exactly the dependents.** A bed: publish an artifact two modules build
|
||
on, and exactly those two are rebuilt.
|
||
- **A stale declaration is refused.** A bed: replay sequence *n−1* after *n*, and the node refuses
|
||
it rather than applying it.
|