011: the provider shape generalises, and two things differ inside it

The broker has all nine properties the store has. So do the object store and the
image registry. A substrate service is a SERVICE PLUS A FACTORY, there are four
of them, and the pattern generalises past the substrate: anything granting
something per consumer has this shape.

Two differences matter more than the similarity.

The broker cannot be managed over the broker. ADR 0001 makes it the channel
every node takes work from and ADR 0039 makes it the security boundary, so the
module providing it is also the way modules are managed — a declaration cannot
be delivered to it over itself. Nothing else has that property; the store is
consumed by the control plane but is not how the control plane REACHES anything.
This is what the carried bundle exists for: the broker is raised from what the
host carries because there is no other way to raise it. A constraint on one
module, not a general rule, and a schema with no way to say so hides it.

And two modules of identical shape want opposite instance counts. The broker is
one per mesh by decision. The store cannot be, because a node that must keep
working while disconnected cannot depend on a database elsewhere. Which settles
what cases.md left open: how many instances is NOT derivable from what a module
is. It is a per-module decision, it has to be declared, and nothing in provides,
requires or excludes says it.

Revocation differs in consequence too. Dropping a database leaves data until
something removes it — a leak, recoverable. Dropping a virtual host loses
whatever was undelivered — silent, and not. Same relation, different blast
radius, which argues for the provider deciding what revocation means rather than
the mesh applying one rule.

File renamed: it was never really about postgres.
This commit is contained in:
2026-08-26 22:54:49 +02:00
parent f160b28a71
commit 13c6068874
3 changed files with 55 additions and 3 deletions
@@ -17,7 +17,7 @@ touches:
Whether the catalogue's missing structure is a **graph** — modules declaring what they need, Whether the catalogue's missing structure is a **graph** — modules declaring what they need,
what they offer, and what they exclude — and what that replaces. what they offer, and what they exclude — and what that replaces.
**[`worked-postgres.md`](worked-postgres.md) works one module through completely**, and breaks **[`worked-provider.md`](worked-provider.md) works one module through completely**, and breaks
the tidy version. A database is nine things, not one — and a small game asking the mesh for its the tidy version. A database is nine things, not one — and a small game asking the mesh for its
own database shows there are **two kinds of edge**: *presence*, where the thing must exist, and own database shows there are **two kinds of edge**: *presence*, where the thing must exist, and
*instantiation*, where a provider makes something for a consumer and hands back credentials. *instantiation*, where a provider makes something for a consumer and hands back credentials.
+1 -1
View File
@@ -1,6 +1,6 @@
# One kind of edge # One kind of edge
> **Superseded in part by [`worked-postgres.md`](worked-postgres.md).** Working postgres through > **Superseded in part by [`worked-provider.md`](worked-provider.md).** Working postgres through
> completely shows there are **two** kinds of edge, not one: *presence* — the thing exists and is > completely shows there are **two** kinds of edge, not one: *presence* — the thing exists and is
> reachable, nothing created — and *instantiation* — the provider is asked to make something for > reachable, nothing created — and *instantiation* — the provider is asked to make something for
> this consumer and hands back credentials. Instantiation implies presence; presence does not > this consumer and hands back credentials. Instantiation implies presence; presence does not
@@ -1,8 +1,10 @@
# Postgres, all the way through # A provider, all the way through
One module, worked out completely, because it is the case that breaks the tidy version. It One module, worked out completely, because it is the case that breaks the tidy version. It
looks like *a container that runs a database* and it is at least nine things. looks like *a container that runs a database* and it is at least nine things.
Postgres is the example. **The shape is not specific to it** — see the end.
## What it carries ## What it carries
**1 — A supervised container.** An image, a version, a data volume, and configuration. Easy, **1 — A supervised container.** An image, a version, a data volume, and configuration. Easy,
@@ -109,3 +111,53 @@ declaration leaves — which makes the host decide something, against
plane reads the node's inventory first and composes with it. The second is consistent and means plane reads the node's inventory first and composes with it. The second is consistent and means
a declaration is composed *per node from what the node reported*, which is a stronger claim than a declaration is composed *per node from what the node reported*, which is a stronger claim than
anything recorded so far. anything recorded so far.
## The same shape, three more times
The message broker has all nine. So does the object store, and so does the image registry. They
differ in what a consumer asks for — a database, a virtual host, a bucket, a repository — and in
nothing structural.
**A substrate service is a service plus a factory.** That is the whole pattern, and there are
four of them. It generalises past the substrate too: anything that grants something per consumer
has this shape, and anything that does not is the simpler case.
But two things differ *between* them, and both matter more than the similarity.
### The broker cannot be managed over the broker
[ADR 0001](../../02-DECISIONS/0001-nodes-communicate-over-a-broker.md) makes the broker the
channel every node takes work from, and
[ADR 0039](../../02-DECISIONS/0039-the-link-is-the-security-boundary.md) makes it the security
boundary — everything a node applies arrives through it.
So the module providing the broker is also **the way modules are managed**. A declaration cannot
be delivered to it over itself, and reconfiguring it is done through the thing being
reconfigured. Nothing else in the catalogue has that property; the store is consumed by the
control plane but is not how the control plane *reaches* anything.
This is exactly what the carried bundle exists for
([ADR 0038](../../02-DECISIONS/0038-a-node-joins-by-linking-first.md)): the broker is raised from
what the host carries, before there is a channel, because there is no other way to raise it.
Recorded here because it is a constraint on *one module*, not a general rule, and a schema with
no way to say so hides it.
### Two modules of identical shape want different instance counts
The broker is one per mesh — a single point of failure and a single point of trust, by decision
rather than by accident. The store cannot be: a node that must keep working while disconnected
([ADR 0036](../../02-DECISIONS/0036-a-node-is-a-managed-machine.md)) cannot depend on a database
somewhere else.
Same nine properties, opposite answers. Which settles something the cases file left open: **how
many instances is not derivable from what a module is.** It is a decision per module, it has to
be declared, and nothing in `provides`, `requires` or `excludes` says it.
### And revocation differs in consequence
Revoking a database leaves data behind until something drops it — a leak, and recoverable.
Revoking a virtual host drops whatever had not been delivered — not recoverable, and silent.
The relation is the same and the blast radius is not, which is an argument for the provider
deciding what revocation means rather than the mesh applying one rule to all of them.