The policy
The version is a path segment. Every endpoint lives under/api/v1.
Three alternatives were considered:
The version constant lives in
packages/contracts/src/http/version.ts. The client
prefixes every request with it, the server mounts routes under it, and the
generated specification is built from it — so the three cannot disagree.
Within v1: additive only
Ship freely:
- new endpoints
- new optional request fields
- new response fields
- new error codes
- anything under a path not yet published
Requiring v2
Ship a new version when:
- a field is removed or renamed
- an optional field becomes required
- a type, range or enum narrows
- a status code changes
- a default changes
- an existing field changes meaning
Shipping both
During a migration the API serves several versions at once. Nothing is switched over globally; clients move when they can. For each superseded operation:- Mark it
deprecated: truein the registry. The generated specification shows it as deprecated, so the reference stops recommending it. - Add the removal date as
sunset(an HTTP-date).DeprecationInterceptorreads both fields off the registry and addsDeprecation: trueandSunsetto every response for that operation, so a client discovers the date from a normal response without reading the documentation (RFC 8594). - Remove it, and only it, on that date.
What a client can rely on
x-api-versionon every response states which version answered. A client can assert it during rollout instead of guessing from a URL.- Within a version, the specification is the contract. It is generated from the schemas the server enforces, so it cannot promise more than the server accepts.
- Unversioned paths do not exist. A request to
/api/customersis a404, not a silent fallback to the current version — a client that lost its version is visibly broken rather than quietly talking to a different contract.
The specification as a migration guide
The diff ofdocs/reference/openapi.json between versions is a field-by-field
account of what changed: names that disappeared, fields that became required,
enums that narrowed. Regenerate it with pnpm openapi:write and review it as
part of the change. See ship a breaking change
for the full procedure.