Skip to content

ADR-0057: A server with no published snapshot refuses, and a device takes its sentinel from the payload

Status: Accepted Date: 2026-09-20

Decisions

D-128 A server that has published no snapshot refuses the read, and never invents a placeholder version

GET /v1/corpus/snapshot and GET /v1/corpus/version both answer 409 Conflict while no snapshot has been published. Neither route synthesises a version, an empty payload or a zero sentinel.

The refusal is never named Corpus Unavailable. That name belongs to the device holding no verifiably complete Corpus (D-72), and it is decided on the device (D-73). This is a statement about the server, and it is a different statement.

D-129 A version read and a snapshot pull are not one atomic read, and a device takes its sentinel from the snapshot payload's own version

The two calls are accepted as two reads. The mitigation is where the sentinel comes from: a client records the version carried inside the snapshot payload it stored, never the value /version answered.

Context

The server was built to refuse and no ADR said so, so three TODO(spec) comments carried the question instead — at serialise-snapshot.util.ts:382, find-version.service.ts:19 and find-published-snapshot.util.ts:27. This ADR is what those comments become a citation of.

The placeholder question is not cosmetic. The sentinel read on every safety read (D-88) is what makes a device's Corpus verifiable, and a device that stored a synthesised version would verify its Corpus against a number the server made up — which is to say against nothing. The Corpus-unavailable ruling (ADR-0032) exists so that "no Corpus" is a state a person is told about rather than one the product papers over.

The naming question is the same ruling from the other side. Collapsing the server's refusal into Corpus Unavailable would undo ADR-0032, which separates the two, and ADR-0036, which spends four distinct answers on the four reasons the app cannot answer. A server-side 409 is not one of those four.

The non-atomic read was found by reading the pull, not by a failure. A publish landing between the version call and the snapshot call would hand a device one Corpus and the other Corpus's version — if the device kept the number from the version call. It does not.

Decision

A server with no published snapshot refuses both reads with 409 Conflict and invents nothing. The two reads are not made atomic; instead a device's sentinel is always the version inside the payload it actually stored, so the two values cannot disagree.

Rejected options

  • Serve a placeholder version — rejected because a client that stores one verifies its Corpus against nothing. The sentinel is the whole of the device-side verification (D-88); a number the server invented turns that check into a formality that always passes.
  • Answer the unpublished case as Corpus Unavailable — rejected because that name is a statement about the device (D-72), decided on the device (D-73). Collapsing the two would undo ADR-0032 and ADR-0036, which exist to keep the reasons the app cannot answer distinct.
  • Make the version read and the snapshot pull one atomic read — rejected because it buys nothing the payload-sentinel rule does not already buy, and adds a transaction to a read that is never on a safety path.

This ruling may not be re-decided

If a change contradicts this ADR: stop and raise it. Do not implement over it.

Specifically: do not return a default, empty or zero version from either route, do not word the 409 as Corpus Unavailable on any surface, and do not take a device's sentinel from /version because it is the value already in hand.

Consequences

What becomes true. A device can never hold a Corpus whose version came from anywhere but the payload it stored, so the sentinel check is a real check. The unpublished server is a named, distinguishable state rather than an empty success. The three TODO(spec) comments have a ruling to cite.

What this costs. A client must handle a 409 from two routes before it has anything to show, and the race between the two reads stays open — mitigated rather than closed. A caller that wanted a single round trip still makes two.

What is closed off. Any synthesised version, and the atomic two-in-one read.

Coverage

UpstreamLanded inEvidenceNote
D-72D-128the refusal is ruled never to be named Corpus Unavailable, because that name is a statement about the deviceADR-0032; the distinction is the reason the placeholder loses
D-73D-128Corpus Unavailable is decided by the device's own startup verification, so the server may not declare itADR-0032
D-88D-128, D-129the sentinel read on every safety read is what a placeholder version would defeat, and what the payload-sentinel rule protectsADR-0036