On this page
Registry
Where does the fleet write down what should be true, and how does a write to it stay safe? The registry is the one canonical document; everything else is observation of it.
What it is
registry.json is the fleet's declaration of what SHOULD be true
(architecture): the versioned compute-target and
coordinator registry, plus the service_directory and placement_profiles
blocks. It is "the single source of truth for every box the queue can route
to" (stado-rs/src/targets/target.rs). One document carries the
targets, the services each target must run, the
service directory, placement profiles, build recipes, and the
fleet queue namespace.
Top-level keys a reader does not model round-trip verbatim through
Registry::extra, so a write never deletes them. This is load-bearing, not
cosmetic: a registry write replaces the whole document, and on 2026-08-04 the
canonical document lost channels, enrollment and fleets exactly that
way — divergent builds writing the same object, each erasing what it could
not name (targets.rs; cli/registry/write/guards.rs).
Who declares it
Operators and the validated write path. stado registry push uploads a local
file; stado service adopt|retire|deploy and stado registry host add edit
the document programmatically through the same path. Validation runs before
any store call, so a document that would not validate never reaches the
registry (cli/registry/write/document.rs, push_document_if).
Import without replacing existing declarations
stado registry import PATH [--json] adopts a complete existing registry-v2 document additively. It is deliberately different from registry push: import never treats omitted source keys as deletions and has no force flag. The importer validates the complete source, including fleet-to-target relationships, before reading canonical storage. Named targets, fleets, coordinators, and placement_profiles use their name as identity; identical names and content are unchanged, missing names are appended, and different content at an existing name conflicts. Other object fields are added recursively; differing scalar values and arrays are conflicts rather than guessed identities. The fully merged candidate is validated again before a conditional write.
Every conflict refuses the whole candidate, exits 75, and retains the prior canonical generation byte-for-byte. Invalid input is rejected, exits 1, and never opens the destination; storage or verification failures also exit 1. A successful write is read back at its returned generation before the receipt is accepted. Repeating a successful import is idempotent and reports unchanged. --json returns stado.registry-import-receipt.v1 with the source SHA-256, previous and accepted generations, exact imported and unchanged targets and fleets, affected sections, and conflict or rejection reasons. Source files remain where the operator selected them and are never modified or copied.
The native app uses the same operation through POST /api/registry/import. That endpoint requires Content-Type: application/json, Content-Length, a body of at most 2 MiB, a registry API bearer explicitly granted registry-import, and the normal trusted dashboard host. It returns the receipt with HTTP 200 for imported/unchanged, 409 for conflict, 400 for rejected, 415 for another media type, and 503 when canonical storage is unavailable. Policy actions do not imply import permission.
Who observes it
Every reader in the fleet. fetch_registry_remote is the fleet-survival
authority — the coordinator's rogue-daemon kill switch and host-health target
resolution read it, with a 30-second in-process cache. It returns a fetch
error rather than an empty registry, because "the store is unreachable" and
"the registry does not list you" drive opposite decisions: collapsing both
into an empty registry is what took the fleet down when the GCP billing
account was closed and the kill switch fired fleet-wide against a registry
nobody had touched (targets.rs).
A reader is not required to die with the authority. Every canonical read that
parses and passes the registry-v2 contract is copied to
~/.stado/cache/registry-last-good.json with a dated sidecar, and readers
that must keep answering serve that copy — carrying its age and one sentence
for the operator. The snapshot bundled with the binary sits below the cache
and is reachable only through the auto loader, announced whenever it is used
(targets.rs).
The copy is scoped to the store that served it. A canonical read from a LOCAL
filesystem store — WC_STORAGE_BACKEND=local — neither records the copy nor
reads it (stado-rs/src/targets/fetch.rs). The copy exists for an authority
on the far side of a network, which can go away while the reader stays up; a
directory on the same disk has no such state, so there is nothing there to
recover from. Two things follow from that scoping. A stado scratch lease
emits a one-target registry into a storage root of its own, and reading it no
longer replaces this machine's fallback copy of the whole fleet — a
one-target document is not a fallback for a fleet. And a lease whose own
document is missing is no longer answered with the operator's fleet, which is
the one registry a disposable account must never be handed.
Where it lives in the store
registry.json at the root of the backend STADO_CONFIG selects
(architecture). The read and write sides address the
same object on every backend; the group used to hardcode a GCS bucket, which
on an Azure-only deployment meant the one document the survival check reads
could be repaired by nobody (cli/registry/mod.rs).
Pushes: compare-and-swap and generations
A push is fenced, refused-by-default, and verified
(cli/registry/write/upload.rs, upload_payload):
- Read the current object and its store generation.
- Refuse a payload that would delete a top-level key the current generation
carries, unless
--force— the 2026-08-04 accident, as a guard. - Refuse a payload whose
service_directory.generationis lower than the one already published, unless--force. That counter is what consumers compare their cached directory against, and it only means something if it never goes backwards: on 2026-08-12 the directory went from generation 10 back to 5 and two corrected endpoints reverted with it. Resolver caches reject generation rollback (architecture). - Compare-and-swap against the read generation (or atomically create when the object is absent), then read back and verify both the generation and the exact bytes.
stado registry pull
stado registry validate registry.json
stado registry push registry.json
Declaration is not observation
The registry declares; it never reports. stado host inventory and the host
beacons are the observation side, and the reconciliation between the two is
deliberately kept as independent axes — declared version vs installed binary,
declared unit vs beacon state, declared endpoint vs consumer probes
(architecture). Software that installs builds onto
hosts without being able to state the declared version, read the actual
version, and name the difference is not automation. A field like a target's
managed_versions is the declaration half; a target that omits it is
reported undeclared, never as agreeing.
Commands that act on it
stado registry validate
stado registry push
stado registry pull
stado registry self
stado registry doctor
stado registry host add HOST --ssh DEST --release-platform PLATFORM
stado registry beacon-age
Full flags in the cli reference.
Not to be confused with
- The service directory. The directory is a block inside the registry with its own generation counter and authority — see directory.
- A beacon. The beacon is what a host observes about itself; the registry is what the fleet declares about the host.
- The object store. The registry is one object in the store, not the store; job state lives under its own prefixes (architecture).
Source: this website