--- topic: what runs on it status: accepted date: 2026-09-28 deciders: jochen reconstructed: false supersedes: 0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md --- # 135. A module version prepares its state before it runs ## Context [ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md) settled who runs a module's migrations and when, and it said so in the wrong vocabulary. It put the declaration on a *container* — "a container may declare steps to run before it" — and derived the scope of the work from the *machine*. Both are wrong at the level a module author works at, and the second is wrong on the facts. **A container is one resource kind the host applies.** A module has code, state and a version; whether its artifact is an image, a bundle or something later is the mesh's business. The module-facing vocabulary for a module's own code already exists and has nothing to do with a container runtime: a module declares **entrypoints** — this file is my tools, this file is my provisioner — and the mesh runs them. A manifest that says "run this container with these arguments, and here are the volumes and environment again" has an author writing down the machine's business twice. **And the scope is not the machine's to decide, because the mesh already decided what a state is.** A consumer is a module *on a machine* (migration 0015, from [issue 022](../04-ISSUES/022-one-credential-per-node-per-provision-not-per-module/00-report.md)): the mesh derives a login per consumer and the provider creates a database owned by exactly that login ([ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)). So a module on three machines is three consumers, three credentials and three databases. There is no shared state for two machines to race over, and ADR 0133's central caveat — that a module's migrations must take a lock because two machines might migrate at once — describes a situation the mesh does not currently produce. That correction makes the whole "level" question HAL answered with stages disappear: the scope of preparation is the scope of the state, and the mesh knows it. What the earlier record got right and this one keeps: the module owns the work, the mesh owns the moment, the gate is the guarantee, migrations stay forward-only, and none of it can be inferred from inside an artifact. What produced it also stands — the control plane was replaced with a build carrying a migration, nothing applied it, and for three quarters of an hour every build was refused by the store with one line that reached only whoever was waiting on a reply ([issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md)). ## Decision **A module version declares an entrypoint that prepares its state.** One name in the manifest, in the same vocabulary as the entrypoints it already declares for its tools and its provisioner. No container, no command line, no environment, no mounts — those are how a machine runs the module's code, and the module already said that once. **The mesh runs it as it runs that module's own code, to completion, in the module's own context.** Every binding, credential and setting the module's code would receive, because it *is* the module's code. How a machine does that is the host's business and stays there: for an image artifact it is the step [ADR 0052](0052-a-step-that-runs-once-before-a-container.md) already defines, and a later kind of artifact changes the host, not the manifest. **Preparation gates the version.** A version whose preparation did not succeed does not run — anywhere. Since the rollout already sends machines one at a time and stops at the first that does not take a version, a preparation that fails stops the rollout there, leaving every other machine on the version that works. **Preparation is scoped to the state, and the mesh derives that scope.** State the mesh provisions is per consumer — a module on a machine — so preparation happens once per consumer. State the module keeps on the machine is per machine, which is the same answer. A module that holds an exclusive seat has one of itself, so its preparation happens once by definition. No level, no election, no cross-node ordering, and no lock obligation invented for a race the mesh does not create. **Once per version per state.** A version bump attempts preparation once against each state it has; the module's own runner decides there is nothing to do, which is what a runner with a version table does anyway. A retry after a partial failure runs it again, so the work is the module's to make safe against that — the one obligation no design can remove. **Forward-only and additive.** Preparation runs while the previous version is still serving, so a migration that removes or renames what the old code reads breaks the mesh in the window between the two. **Declared, never inferred.** The control plane cannot see inside an artifact, so a module that ships migrations and declares no entrypoint is not refusable at registration. It breaks on its first upgrade, and this record says so rather than implying a check that cannot exist. ## Options considered 1. **A container declares steps before it** — [ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md). Superseded, not because the mechanism is wrong but because the *declaration* is in the wrong place: it makes every module author restate the machine's arrangement, and it ties a module's own lifecycle to one resource kind. The host-side mechanism it named is retained and is now an implementation detail. 2. **Each module prepares itself when it starts** — what the catalogue does today. Rejected: a schema failure becomes a crash loop rather than a stop, nothing in the declaration says the module has a state to prepare, and the version serves the moment it starts rather than after the state is right. 3. **The mesh applies migrations itself**, with a driver and a version table per store type. Rejected: the mesh would have to know one store from another, hold another module's credentials and reach a machine with them, which [ADR 0005](0005-the-node-host.md) forbids. It is also what forces a stage system: something central has to decide where the work happens. 4. **A hook lifecycle** — pre-build, post-build, pre-deploy, post-deploy. Rejected: a declaration is a desired state reconciled forever, so there is no deploy moment to hook. "Pre-deploy" is exactly this record; pre- and post-build are what a recipe and the artifact list already are; "post-deploy" names nothing that happens. 5. **A declared level** — once per module, or once per assignment. Rejected: the mesh already knows what a state is, so asking an author to choose is asking them to restate a fact the mesh holds, with a chance of contradicting it. 6. **Record a preparation level per module in the store.** Rejected for the reason ADR 0133 gave and this record keeps: gating makes the invariant true by construction, and a level is a second account of the same fact. ## Consequences **An author's whole contract is one line, once.** Write the migration in the module's code, name the entrypoint that runs it, and every later version rolls out as: build, prepare, run — with nothing per-version to remember and nothing about the machine to restate. That is the property this exists for. **Three hand-written steps in the catalogue collapse**, and the control plane's own migrate step stops repeating its server's environment and mounts. **The catalogue's self-preparation becomes the exception to remove.** One shape, and the mesh's own control plane is not an exception either. **A module scaled across machines with one shared state is not expressible**, and this record does not make it so. The mesh gives each consumer its own state; a deliberately shared one is a different provision model, and the place the "once, mesh-wide" question would genuinely return. Named here so it is a decision when it happens rather than a surprise. **There is still no readiness-gated step.** Only an action carries `verify`; nothing declares that a service answers, so preparation that must happen *after* something is serving — seeding through its own API — remains unexpressible. **Genesis keeps its own action.** At birth there is no control plane to derive anything, which is what [ADR 0067](0067-genesis-is-a-pivot.md) says about that moment. ## How this is checked - **The composition carries the preparation, in the module's own context.** A test on a node's composed declaration: a version declaring a preparation entrypoint is preceded by it, and what it is given equals what the module's own code is given — asserted equal rather than written twice, which is the drift the superseded shape invited. - **A preparation that fails stops the version.** The host does not go past a step that did not complete, and the rollout stops at the first machine that did not take a version. Both are existing behaviours with existing tests; the test for preparation asserts the two together — the machine does not run it, and the machines after it are left alone. - **Once per version per state.** A test that a second convergence of the same version prepares nothing, and that a new version prepares again. - **The mesh's own control plane declares one.** The case that failed on 2026-09-28 is the case the tests cover, rather than a case a comment says is covered. ## References - [ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md) — what this supersedes, and why - [ADR 0052](0052-a-step-that-runs-once-before-a-container.md) — the host-side step that implements it for an image artifact - [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md), [issue 022](../04-ISSUES/022-one-credential-per-node-per-provision-not-per-module/00-report.md) — a consumer is a module on a machine, which is what makes the scope derivable - [ADR 0018](0018-a-picture-is-read-from-what-runs.md) — a digest is the record that something happened - [ADR 0005](0005-the-node-host.md) — the control plane decides and never touches a machine - [ADR 0134](0134-the-mesh-says-what-it-applied.md) — what makes a failed preparation visible - [issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md) — the failure that produced both records