Bitspark constellation
accepted source ↗

ADR 0032 — the service base API: a uniform …z + identity surface every member serves

  • Status: accepted
  • Date: 2026-06-21
  • Builds on: 0011 (collect-never-invent — the runtime probe that reads this surface and grades the gaps as findings), 0026 (the runtime registry that says which (system, environment) to probe; pharos attaches the environment a report must not self-claim), 0027 (/versionz is its SUBSTRATE_MIGRATION §4 self-report + the Gate-C observed-epoch source)
  • Relates: 0028 (/identity advertises the service public key the single-root model grants authority TO), 0030 (a provider authenticates the runtime — /identity is the discovery seam for the keys involved), 0016 (/identity is lifted from arche-facts, which stele's node mirrors)

Every constellation member that serves HTTP serves a uniform base API. Two endpoints are universal — /healthz (liveness: the process answers) and /versionz (the 0027 self-report, schema atlas-versionz.schema.json). Two more are required of every substrate participant (a node or a fact-writing client) — /readyz (readiness: admitted/serving, distinct from liveness) and /identity ({"identity":"<base64 Ed25519 public key>"}, the discovery seam a peer or the root hits to learn the key the service signs facts with). The control plane (pharos) serves /healthz + /versionz + /readyz but not /identity — it holds no substrate signing identity (it verifies others', never asserts as one). Atlas owns the schemas + the conformance probe; each service owns the report it serves; pharos/atlas consume. This is one surface, not four ad-hoc ones — so discovery, readiness gating, version grounding, and key lookup are the same call everywhere.

Context

The pieces already existed, unevenly. /versionz is fully specified — atlas-versionz.schema.json

  • the atlas substrate versionz validator + pharos's collector probing it fleet-wide and enforcing that a service cannot claim to be another (ADR 0027 §4). /healthz and /readyz are probed by the atlas runtime collector (ADR 0026/0011: runtime/unreachable, runtime/endpoint-not-admitted). /identity is the stele node's discovery endpoint, itself lifted from arche-facts: GET /identity → {"identity":"<base64 node pubkey>"}.

But adoption was a patchwork. A survey of the running fleet:

service archetype /healthz /versionz /readyz /identity
stele substrate node (reference)
corpus substrate client (provider)
kosmos substrate runtime (adstrate)
pharos control plane (n/a)

The gap that surfaced this: to repoint corpus/production at a new stele node, the root must stele grant corpus's identity — but corpus exposes no way to learn its own public key (it is derived from a sealed seed). The one-off fix would be "add /identity to corpus." The right fix is to notice this is the same discovery seam every substrate participant needs, and that the whole base surface should be a named, conformance-checked standard rather than whatever each repo happened to ship. It spans every member and has no single-repo owner — so its seat is atlas (the membership test).

Decision

1. The universal pair (every HTTP-serving member).

  • GET /healthz — liveness only: the process is up and answering. Unauthenticated, never gated by any lock/readiness. Body ok. It says nothing about whether the service can do useful work.
  • GET /versionz — the service self-report, conforming to atlas-versionz.schema.json (required: service, revision; the service reports neither its environment nor a claim to be another member — pharos attaches the (system, environment) it probed). This is the 0027 lifecycle inventory / format-capability surface; it is what makes Gate C observed rather than derived.

2. The substrate-participant pair (a node, or a client that writes facts under an identity).

  • GET /readyz — readiness, distinct from liveness: 200 body ready when the service can serve its purpose; 503 while it cannot yet. For a node, "ready" = its endpoint self-assert was admitted (the root granted the node identity); a service with no pending-admission state is ready as soon as it serves (the nil-readiness path). Unauthenticated; reports 503 rather than erroring while not-ready, so a prober can distinguish "down" from "up-but-not-ready."
  • GET /identity{"identity":"<base64-std Ed25519 public key>"}, the service's substrate public key: the key it signs facts/bindings with, the thing the root grants authority to and a peer authenticates. Public — safe to serve unauthenticated (it is the published half). Mounted only when the service has an identity: a service running without one (a dev/in-memory mode) does not mount the route (404), exactly as stele gates it on having a key. Never serves the private half.

3. The control plane (pharos) is exempt from /identity. pharos actuates and verifies other services' identities; it holds no substrate signing identity of its own, so it has nothing to advertise. It serves /healthz + /versionz + /readyz (readiness = its data plane is up: the encrypted vault is unlocked and the store is attached), and /readyz is exempt from the locked gate so it can truthfully report 503 while locked.

4. Ownership & conformance. Atlas owns the schemas (atlas-versionz.schema.json, atlas-identity.schema.json) and the conformance probe: the runtime collector reads this surface for every registered (system, environment) and grades a missing/malformed endpoint as a graded finding (ADR 0011 collect-never-invent; a live-ops lapse is a warning, never a committed-baseline lie). Each service owns the report it serves; pharos and atlas consume it. Archetype is read from the member's place in the model (a runtime env carrying a rootPubkey/identity is a substrate participant; the control plane is named).

Consequences

  • One surface, four guarantees, everywhere. Discovery (/identity), readiness gating (/readyz), version grounding (/versionz), and liveness (/healthz) are the same call against any member. Tooling (pharos's collector, atlas's probe, a human with curl) stops special-casing.
  • It unblocks cross-service grants by construction. corpus gaining /identity is not a one-off — it is corpus conforming. The root learns any participant's grantee key from one endpoint, so stele grant <pubkey> no longer needs the sealed seed extracted; the discovery seam is the standard.
  • Conformance becomes a doctor gate. A member that ships without /versionz or (as a participant) /identity//readyz shows up as a finding, the same way a stale figure or an unreachable node does.
  • Cost. Each service maintains four endpoints and keeps its /versionz schema-valid; the probe is one more thing the collector does per env. The archetype rule is a heuristic over the model, not a per-service declaration — if a member ever needs a non-obvious archetype, that becomes an explicit field (recorded open, below).

Open questions (recorded, not decided)

  • Archetype as data. Today archetype (participant vs control-plane) is inferred from the model (identity-bearing runtime env vs the named control plane). If a third archetype appears, or the inference misfires, promote it to an explicit manifest field rather than widening the heuristic.
  • /readyz depth. "Ready" is currently shallow (admitted / data-plane-up). Whether it should also assert downstream dependency health (a node's DB, a provider's substrate link) is deferred — start shallow, deepen per service if a real outage would have been caught by it.
  • One more endpoint? A /metricsz (or aligning on OpenMetrics) is plausible but out of scope here; this ADR fixes the identity/lifecycle/health quartet only.

The Bitspark constellation — how the systems are built and relate.

GitHub