Registry stack documentation: machine-readable Markdown.
Index of all pages: https://docs.registrystack.org/v/0.38.0/llms.txt
Full corpus: https://docs.registrystack.org/v/0.38.0/llms-full.txt

# API stability and versioning

> The compatibility promise Registry Stack makes at v1.0.0, the surfaces it covers, and what counts as a breaking change.

This page defines the compatibility promise for Registry Stack releases: which surfaces are
covered, what counts as a breaking change, and which surfaces stay exempt. The promise takes
effect at `v1.0.0`. Until then, Registry Stack is a pre-1.0 technical release and the
[pre-1.0 rule](#versioning-scheme) applies.

## Versioning scheme

Registry Stack releases carry one stack-wide version, tagged `vMAJOR.MINOR.PATCH`. Every
released artifact of a release (binaries, container images, the docs snapshot, the release
manifest under `release/manifests/`) shares that version.

Before `v1.0.0`:

- A minor release may contain breaking changes. Each one is announced with a `BREAKING:` entry and
  concrete migration steps in the stack release note, plus the owning product CHANGELOG where one
  exists.
- A patch release is backward compatible with the latest minor line.

From `v1.0.0`:

- A major release is the only release that may break a covered surface.
- A minor release may add surface (new routes, new optional config keys, new CLI flags,
  new error codes) and may deprecate surface, but not remove or change it incompatibly.
- A patch release contains fixes only.

## Covered surfaces

The promise covers the surfaces in this table. For each, the contract artifact is the source
of truth: if this page and the artifact disagree, the artifact wins.

The enforcement column names the repository CI checks so the mechanism is auditable.

| Surface | Contract artifact | Enforcement today |
| --- | --- | --- |
| Relay HTTP API (the fixed route inventory and the response envelope it carries) | The closed route list built by `router` in `crates/registry-relay-v2/src/server.rs` and the generated-artifact contract `products/relay-v2/contracts/artifact-inventory.yaml`. Relay has no product-level OpenAPI document to pin, because the resources a document describes come from the adopter's own Registry contract; each deployment serves its own description at `GET /openapi.json`. See [API references](../apis/) | Root CI's `relay-v2-contracts` job runs `products/relay-v2/scripts/check-contracts.sh` and `products/relay-v2/scripts/test-http.sh`, which replay the four coequal acceptance projects under `products/relay-v2/acceptance/` against their recorded `expected-http.yaml` exchanges |
| Relay authoring grammar and sealed package format | The Registry contract grammar `relay.registrystack.org/v2alpha1`, covering `registry.yaml` (`kind: RegistryContract`), the deployment binding grammar `registry.registrystack.org/relay-runtime/v1alpha1` covering `runtime.yaml` (`kind: RelayRuntimeConfig`), and the sealed package in the shared Registry Stack package format. The grammars carry pre-1.0 version markers today; the promise attaches to the version each one carries at `v1.0.0` | `RegistryContract::parse_yaml` and `RelayRuntime::parse_yaml` reject unknown keys rather than ignoring them (`crates/registry-relay-v2/src/contract.rs`); `products/relay-v2/scripts/check-configs.sh` holds each acceptance project against its recorded contract revision, package digest, and artifact digests in `products/relay-v2/contracts/generated-baselines.yaml`; `relay serve` verifies a package before it activates |
| Evidence Gateway HTTP API and its Version 1 contracts | The frozen Version 1 source contracts under `products/evidence/contracts/`, indexed by `products/evidence/contracts/README.md`, and the artifacts generated from them under `products/evidence/generated/`, including `registry-evidence.openapi.json` | Root CI's `evidence-contracts` job runs `products/evidence/scripts/check-contracts.sh`, which regenerates every contract artifact and fails on any byte difference from the committed copies, plus `products/evidence/scripts/check-source-neutrality.sh` and `products/evidence/scripts/check-verifier-portability.sh` |
| Error contract and stable identifiers | RFC 9457 problem shape with the stable `code` member, the [error registry](../errors/), and the `https://id.registrystack.org/` identifier space. Relay's set is closed by `ProblemCode` in `crates/registry-relay-http-contract/src/lib.rs`. Evidence Gateway's closed problem set has its own frozen contract, `products/evidence/contracts/problem-contract.yaml`, documented at [Evidence Gateway problem types](../evidence-problems/) | Relay's code, status, title, detail, and type URI come from one exhaustive catalog, and product tests pin the rendered body and headers; the `evidence-contracts` job regenerates and byte-diffs the committed Evidence Gateway problem schema against runtime-owned generation |
| Configuration formats and documented environment variables | Relay's `registry.yaml` and `runtime.yaml` grammars, the frozen Evidence Gateway schemas `products/evidence/contracts/runtime.schema.yaml` and `products/evidence/contracts/bundle.schema.yaml`, plus the [environment variable reference](../environment-variables/) | Relay and Evidence Gateway both parse with `deny_unknown_fields`, so a retired or misspelled key is a startup refusal rather than an ignored field; Evidence Gateway's config parser is tested against the frozen contract schemas (`crates/registry-evidence/src/config.rs`), and `products/evidence/scripts/check-config-key-paths.sh` holds its configuration reference in exact parity with them |
| Registry Manifest schema and rendered artifacts | `registry-manifest/v1` and the rendered artifact schema versions, governed by [RS-DM-MANIFEST](../../spec/rs-dm-manifest/) | `validate_manifest` accepts only `registry-manifest/v1`; REQ-DM-MANIFEST-013 requires strict unknown-key rejection at parse time |
| Command-line interfaces | Documented commands and flags of `relay`, `evidence`, and `registry-manifest`, and their machine-readable output modes | CLI reference pages; the `evidence` command surface is stated in `products/evidence/OPERATOR-CONTRACT.md` and exercised end to end against the built binary in `crates/registry-evidence/tests/cli.rs`; the release candidate workflow asserts each built binary reports the release version |
| Release artifacts and verification interface | Released binary asset names, the version-appropriate `ghcr.io/registrystack/<image>` roster recorded in the release manifest, and the signature and provenance layout in `release/VERIFY.md` | Release workflow; signed assets and exact image digests verified as documented in [SECURITY.md](https://github.com/registrystack/registry-stack/blob/v0.21.0/SECURITY.md) |

Relay's covered HTTP surface is its route inventory, not the content of any one deployment's
OpenAPI document. Adding a resource, an operation, or a statistical dataset to a Registry contract
changes what one deployment answers; it does not change the product surface, and removing one is a
decision for the institution running that deployment rather than a stack release event.

No shipping binary currently exposes a covered metric family. Relay serves no metrics route, and
`release/contracts/selected-metrics.json` now carries an empty metric list, so the
selected-metrics mechanism defined in this page has nothing under it until a maintained product
publishes a family through it.

`evidence-oid4vci` is absent from the table because it is a supporting service rather than a
pattern of its own. Its configuration fields, route shapes, and CLI flags may still change without
a major release.

## What counts as a breaking change

For HTTP APIs:

- Removing or renaming a route, parameter, request field, or response field
- Changing the type, meaning, or optionality of an existing field
- Removing or renaming a stable error `code`
- Changing the stack-wide meaning of a released error code, or changing an existing
  product-and-operation HTTP mapping for that code
- Tightening authentication or scope requirements on an existing route, except as a security
  fix announced in the release notes

For configuration:

- Removing or renaming a config key or documented environment variable
- Narrowing the accepted values of an existing key
- Changing the semantics or the default of an existing key (see
  [Defaults are part of the contract](#defaults-are-part-of-the-contract))

For CLIs:

- Removing or renaming a documented command or flag
- Changing the schema of a machine-readable output mode incompatibly

For the Relay authoring grammar and sealed package format:

- Removing or renaming a document key, or changing the meaning of an existing one
- Rejecting a project that the previous release compiled and sealed under the same profile
- Changing what a compiled artifact contains for an unchanged project and source schema

For a selected metric family, once a maintained product publishes one:

- Removing or renaming a family
- Changing a family type, label key, or label meaning

Metric help text, output ordering, and corrections to observed values are not covered. New
families and new values for an existing bounded label are additive changes.

Additive changes (new routes, new optional fields, new optional config keys, new error codes,
new commands and flags) are minor-release material and are not breaking.

## Compatibility direction

The promise is backward compatibility: a new binary within a major line reads the wire data,
config files, manifests, and persisted state written for any earlier release of that line.
Every release documents a forward state path from its immediate predecessor.
An upgrade that skips releases follows each sequential hop unless the target release explicitly
certifies a direct path.

The promise starts at `v0.33.0`. The one exception is v0.32 to v0.33: no adopter ran v0.32, so
`v0.33.0` shipped without a forward state path from it, and state written by v0.32 or an earlier
release has no supported upgrade.

{/* Evidence: release/scripts/rehearse-upgrade.py, FORWARD_PATH_FLOOR and FORWARD_PATH_EXCEPTION. */}

The reverse is not promised. Relay and Evidence Gateway parse config with
`deny_unknown_fields`, so a config file that uses keys introduced in a newer release fails to load
on an older binary. Pin your config to the release you deploy.

None of the three products this page covers, Relay, Evidence Gateway, and Registry Manifest, has
migrated state or a migration command. Relay owns no database: it
reads the adopter's SQLite source through a read-only boundary and writes the audit log named by
the `audit` block in its runtime file. Evidence Gateway writes the audit log named by the `audit`
block in its runtime file. Relay's sealed package is an input to verify rather than state to migrate. `relay` recompiles the
packaged contract and regenerates its artifacts at startup, and refuses to activate a package they
do not reproduce, so a package-format change is a repackage of the authoring project rather than a
conversion of an installed package. A directory that still carries `relay-package.json` instead of
`SHA256SUMS` is refused with a message naming `relayctl package`.

Base Registry Engine, Casework, and Scheduling are separate runtime products outside this page's
covered surfaces, and each owns migrated PostgreSQL state with its own migration command:
[`bregctl apply`](../../operate/breg-changes/#activate-the-successor) activates a package and runs
its migration, [`caseworkctl apply`](../../operate/casework/#plan-apply-and-serve) activates a
Casework package and runs its migrations, and `schedulingctl apply` activates a Scheduling package
and runs its migrations. Before 1.0, an upgrade between releases of any of the three can require the
migration steps that release's notes name; read them before upgrading.

## Defaults are part of the contract

A default value is behavior an operator relies on without writing it down.
Changing a covered default incompatibly requires a major release, except for a documented
security removal under the [deprecation policy](../deprecation-policy/).

## Exempt surfaces

The following are not covered by the compatibility promise, at any version:

- Rust crate APIs. Every workspace crate is `publish = false`; nothing is published to
  crates.io. Consumers who pin the repository by tag or commit get exactly what they pinned,
  and crate-level APIs may change in any release.
- Human-readable CLI output, log message text, and problem `detail` strings. Machine
  consumers must use the JSON output modes, the structured log format, and the `code` member.
- Hidden commands and internal protocols. A command or protocol that no reference page documents
  is not a covered surface.
- Adopter tooling that wraps a covered surface rather than defining one. `relayctl` initializes,
  compiles, and seals an authoring project and leaves verification, activation, and serving to the
  `relay` binary; its report JSON is best-effort local automation rather than a contract, as the
  [relayctl command reference](../relayctl/) states. `evidencectl` generates key material and
  project scaffolds, delegates runtime decisions to the `evidence` binary, and reuses the Evidence
  client and portable verifier for relying-party request preparation and offline response
  verification.
- The layout of the repository, the `products/` export trees, the docs site internals, and
  the lab tooling.
- Descriptions, summaries, and ordering inside generated OpenAPI documents. The contract is
  the paths, operations, schemas, and status codes, not the prose.

## Route version namespaces

The HTTP surface carries independent version prefixes per product and per plane. Each namespace
versions independently; a version bump in one namespace does not imply one in another.

Relay carries two: `/v2` for the Registry Record data plane and `/sdmx/v2` for the statistical read
plane. Both are served by one listener. Relay has no admin plane, no separate admin listener, and
no metrics route, so no third namespace exists to version. Its `/health`, `/ready`, and
`/openapi.json` routes are operational and discovery routes, unversioned by convention.

Evidence Gateway's three versioned operations, `POST /v1/evidence`, `POST /v1/evidence/batch`, and
`GET /v1/evidence-definitions`, are the whole of its evidence surface under the frozen Version 1
contract. Its remaining routes are
operational or discovery routes and are unversioned by convention: `/health`, `/ready`,
`/openapi.json`, `/.well-known/evidence/jwks.json`, and `/.well-known/jwt-vc-issuer`. Evidence Gateway
serves `/metrics` on a separate listener when one is configured, never on the request listener.

## Enforcement

The promise is machine-checked where a checker exists:

- Relay: root CI's `relay-v2-contracts` job runs `products/relay-v2/scripts/check-contracts.sh`,
  which validates the product contracts, the SDMX profile lock, and source neutrality, then holds
  each acceptance project against its recorded contract revision, package digest, and artifact
  digests. The same job runs `products/relay-v2/scripts/test-http.sh`, which replays the recorded
  HTTP journeys for the four acceptance projects and the SDMX read profile against the runtime.
- Evidence Gateway: root CI's `evidence-contracts` job regenerates every contract artifact from the code
  with `products/evidence/scripts/check-contracts.sh` and fails on any byte difference from the
  committed copies, so the OpenAPI document, the problem schema, and the request and response
  schemas cannot drift from a released binary.
- Config: Relay's grammars reject unknown keys at parse time rather than ignoring them, and
  Evidence Gateway's config parser is tested against the frozen contract schemas rather than a schema
  generated from it.
- Forward state path: the release upgrade rehearsal workflow runs
  `release/scripts/rehearse-upgrade.py` on every pull request that changes a release manifest, and
  on manual dispatch. It downloads the previous release's Base Registry Engine, Casework, and
  Evidence Gateway binaries, verifies them against the signed `SHA256SUMS` as `release/VERIFY.md`
  describes, and writes records, review work, and audit entries with them against disposable
  PostgreSQL. It then runs the upgrade steps with the binaries built from the pull request and
  fails when captured domain data or stored revisions change, other captured views differ, or
  retained tables lose rows. Base Registry Engine rebuilds and applies a successor before serving; its
  package-bound ETags may change. Audit
  tables retired by this breaking upgrade must first be preserved in an archive whose row counts
  are checked. Old audit files are preserved separately from the new entry stream. It refuses to
  start from a release before `v0.33.0`. Scheduling state is not rehearsed: the rehearsal verifies
  release runtime binaries, and `v0.33.0` through `v0.37.0` shipped the Scheduling runtime only
  as a container image. Its standalone runtime binary joins from `v0.38.0`.
  Retiring Scheduling's audit outbox is covered instead by migration 8, which refuses while the
  outbox holds unpublished rows, and by the operator archiving the old audit file, as
  [Retention and persistent state](../../operate/retention-and-persistent-state/) describes.

{/* Evidence: .github/workflows/release-upgrade-rehearsal.yml; release/scripts/rehearse-upgrade.py, fetch_release(), breg_view_differences(), view_differences(), row_count_losses(), and check_forward_path(); crates/registry-scheduling/src/store.rs, AUDIT_WRITER_MIGRATION_VERSION. */}

A change that a gate flags as breaking lands only in a release whose version number permits
it, together with the deprecation and migration steps required by the
[deprecation policy](../deprecation-policy/).

## Next

- [Deprecation policy](../deprecation-policy/)
- [Security support window](../../security/support-window/)
- [Operate Relay](../../operate/relay/)
- [Contracts](../contracts/)