Versioning
Three versions, moving independently, with one rule each.
| Axis | Looks like | Changes when | Breaking? |
|---|---|---|---|
| API contract | /wine/v1/…, X-API-Version | a breaking change to the request or response contract | yes, by definition |
| Dataset | ttb-permits-2026.1 | upstream data or the build changes | no — never on its own |
| Service release | 1.2.0 | any deploy | no |
Machine-readable: /wine/v1/version and /wine/api/v1/versions.json.
The test that settles arguments
Additive — no version bump
- a new endpoint, or a new response field
- a new optional request parameter
- a new enum value in a field documented as open-ended
- widening validation — a request that used to fail now succeeds
- correcting data values, or refreshing a dataset to a newer upstream edition
Breaking — new path version, old one keeps serving
- removing or renaming a response field
- changing the type or units of a field
- removing an endpoint, or changing its path
- adding a required request parameter
- tightening validation — a request that used to succeed now fails
- changing a default value, page size or sort order
- changing the meaning of a field without renaming it
- changing the shape of a record inside a published dataset
That last one is the subtle one: the static datasets are published files, so people build against a producer record's shape exactly as they build against an endpoint. Adding a field to it is additive; renaming one is breaking, even though no route changed.
Deprecation flow
current ──► deprecated ──────────────────► sunset ──► gone
(≥6 months notice) (≥12 months total support)
From the moment of deprecation, every response on the old version carries:
Deprecation: Wed, 01 Apr 2026 00:00:00 GMT (RFC 9745)
Sunset: Thu, 01 Oct 2026 00:00:00 GMT (RFC 8594)
Link: <https://apis.allanninal.dev/wine/versioning.html>; rel="deprecation"
…plus a deprecation block in the JSON body, because a client reading only the body should not be the last to find out.
Reproducibility
Every response carries datasetVersions. Any response can be regenerated from those
versions plus the repository at the matching tag — the raw upstream payloads are cached and the
build is a pure function of them. A rebuild that changes output when upstream did not is a bug,
and the validation gate is what catches it.
How it is enforced
Not by memory. scripts/check-contract.mjs boots the app, walks every route,
response shape and dataset record shape, and diffs them against a recorded snapshot. Anything
breaking fails the build. It exists because the change that hurts a caller is never the one you
meant to make.