Files
hq/01-RESEARCH/011-the-module-graph/worked-postgres.md
T
jschoubben f160b28a71 011: postgres worked through, and "one kind of edge" was wrong
The tidy version said a module provides names and requires names and that is the
only edge. Working postgres through completely disproves it.

A small game wanting to store data does not require postgres to EXIST. It
requires postgres to MAKE IT A DATABASE and hand back credentials. Those are
different relations in every way that matters: one creates something per
consumer, carries a payload back, can be revoked, and leaves the provider
holding state about who was granted what. The other creates nothing.

So: two kinds of edge, one graph. Instantiation implies presence; presence does
not imply instantiation. The current system already had exactly this split —
`dependencies` for presence, `requires: provision:` for instantiation, with the
resolver deriving one from the other. analysis.md called that derivation a
convenience. It is not: it is the correct relationship between two genuinely
different relations, and the design had collapsed them.

Postgres also turns out to be nine things, not one. A container. Persistent
state where moving nodes is a migration rather than a reschedule. Configuration
partly derived from the machine's hardware. A tool surface. A provisioner. Its
own bookkeeping about what it granted, which is not the data it stores. An
exposure decision per node it runs on. Credentials it generates, which means a
provisioning edge carries a secret. And health that is not "the container is up".

Four questions the worked example makes concrete rather than abstract. WHICH
postgres, when there are two — a consumer of `terminal` does not care and a
consumer of a database cares permanently. How many instances a module should
have, which cannot be a global rule because one-per-mesh is wrong for a store a
disconnected node needs and one-per-node is wrong for the mesh's own registry.
What happens to a grant when its consumer is removed, where dropping is data
loss and keeping is a leak. And whether a declaration is composed PER NODE from
what that node reported — because tuning follows hardware the control plane
cannot know, and the alternative is the host deciding, which ADR 0037 forbids.
2026-08-26 22:53:59 +02:00

6.0 KiB

Postgres, all the way through

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.

What it carries

1 — A supervised container. An image, a version, a data volume, and configuration. Easy, and the only part the phrase "a docker service" describes.

2 — Persistent state, and where it lives matters. The volume is the database. Moving this module between nodes is not rescheduling; it is a migration. Almost nothing else in the catalogue has this property, and nothing in provides / requires expresses it.

3 — Configuration that is partly the machine's. Tuning follows the hardware — memory, storage. A declaration generated centrally cannot know those, and research 012 says the machine's own values win on conflict. So some of this module's configuration is derived from the node it lands on.

4 — A tool surface. It exposes capabilities agents can call — query, list, provision. That is not a resource on a machine and not an artifact; it is a contract the mesh publishes on the module's behalf.

5 — A provisioner. The part that matters, and the one below.

6 — Its own bookkeeping. The provisioner must remember what it granted to whom, or it cannot revoke, rotate or clean up. So a module that provides state to others also holds state about its providing — and that state is not the database's data.

7 — An exposure decision, per node it runs on. Reachable from the machine only, from the local network, or publicly. That is a property of this assignment, not of the module: the same module on two nodes may answer differently.

8 — Credentials it generates. Per consumer, and they have to reach the consumer. Which means a provisioning edge carries a payload, and the payload is a secret.

9 — Health that is not "the container is up". A container running and a database accepting connections are different facts, and the second is the one anything cares about. This is the host's read-back rule, at a distance.

The provisioner is a second kind of edge

The tidy version of this effort said: a module provides names, a module requires names, that is the only edge. A small game wanting to store data shows it is not.

my-cool-game   requires   postgres            # I speak its protocol, it must exist
my-cool-game   requires   a database FROM postgres, called my-cool-game

The first is presence: the thing exists and is reachable. Nothing is created; nothing flows back. vscode requires terminal is this, and so is requires container-runtime.

The second is instantiation: the provider is asked to make something for this consumer, and hands back what the consumer needs to use it. A database, a user, a password, an address.

They differ in every way that matters:

presence instantiation
creates something no yes, one per consumer
carries a payload back no credentials, an address
can be revoked — yes, and must be when the consumer goes
provider holds state about it no yes — who was granted what
satisfied by anything providing the name that provider, specifically

This is the mesh's actual power, in the operator's words: a small game declares it wants a database and the mesh makes one. Nobody creates a user by hand, nobody pastes a connection string. That is worth being precise about rather than folding into a single relation because one relation is prettier.

What that costs the design

The proposal's "one kind of edge" is wrong, and the current system already knew: it has dependencies for presence and requires: provision: for instantiation, with the resolver deriving a presence edge from every instantiation edge. analysis.md recorded that derivation as a convenience. It is not — it is the correct relationship between two genuinely different relations.

So: two kinds of edge, one graph. Instantiation implies presence. Presence does not imply instantiation.

What still has no answer

Which postgres? A mesh with two of them, and a game that wants a database. Presence would be satisfied by either. Instantiation cannot be — the data will live in exactly one, and choosing wrongly is not a preference, it is the game's data in the wrong place, discovered later.

This is the who chooses between providers question from proposal.md, and the worked example shows it is far sharper for instantiation than for presence. For terminal the consumer genuinely does not care. For a database it cares permanently.

How many instances of postgres should exist? One per mesh is wrong — a node that must work while disconnected cannot depend on a database elsewhere. One per node is wrong — the mesh's own registry is one thing, not one per node. So the answer is per-module, and nothing in the schema says it. This is cases.md axis how many instances, and postgres is the case that proves it cannot be a global rule.

What happens to a grant when the consumer is removed? The game is uninstalled. Its database still exists, holding its data. Dropping it silently is data loss; keeping it forever is a leak. ADR 0043 says the host removes what it applied and no longer declares — but this is not on the host, it is inside another module's state, and the same reasoning does not obviously carry.

Where does node-derived configuration come from? (3) The control plane composes a declaration, and cannot know this machine's memory. Either the host fills in a blank the declaration leaves — which makes the host decide something, against ADR 0037 — or the control 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 anything recorded so far.