ADR 0001 — Platform version, API contract version, and OpenAPI spec version are three independent numbers
Date: 2026-05-23
Status: Accepted; the platform version's source superseded 2026-08-09 by ADR 0004
Tracking: TRA-485 (mechanism), TRA-672 (info.version semantics), TRA-481 (/health SHA exposure), TRA-1126 (source superseded)
2026-08-09 — superseded in part, not reversed. The decision this record exists for — that the platform version, the API contract version and the OpenAPI
info.versionare three independent numbers, and that the monolith has exactly one platform version fed to every surface from a single source — stands entirely. What changed is where that single source lives.
git describe --tags --always --dirtyat build time made the version a property of the ref that built the image rather than of the commit, which broke three releases in three different ways. It is now declared in a rootVERSIONfile and the git tag is an output of the release build. ADR 0004 has the reasoning.Read the Decision and Consequences below with that substitution: wherever this record says
git describe, the mechanism is now theVERSIONfile. The Alternatives section is unaffected — the runtime-fetch and release-please options were rejected for reasons that do not depend on the source.
The TrakRF monolith historically carried three different version numbers, none of them synchronised:
frontend/package.jsonversion—1.0.18, climbed for legacy reasons inherited from the standalonetrakrf-handheldrepo, surfaced asv1.0.18in the nav header.- Backend
main.version— hardcoded0.1.0-previewin the root Dockerfile and0.1.0-devinbackend/justfile, surfaced at/health. - OpenAPI
info.versionin the published spec — set to1.0.0per the Zalando must-use-semantic-versioning convention (TRA-672), bumped manually when the spec ships a breaking change.
The first two were nominally describing the same artifact — the monolith ships the frontend embedded in the backend binary, so any commit produces exactly one deployable. Two numbers a whole major apart was incoherent. The third was describing something else entirely and was correctly decoupled, but the prior state made that easy to forget.
The URL path version /api/v1/ is a fourth axis but is not a number — it is a
long-lived URL contract that flips to /api/v2/ only on a real breaking change
to the customer-facing API surface. It is mentioned here only to clarify it is
not the same as info.version.
The monolith has one platform version. It is the output of
git describe --tags --always --dirty at image-build time, injected once and
fed into every surface that needs to name the running build:
- Go binary — via
-X main.versionldflags, reported at/health. - Frontend bundle — via
VITE_APP_VERSION, rendered in the nav header and the Settings screen. dist/version.json— emitted by the Vite plugin socurl host/version.jsonmatches/health.
The CI workflow (.github/workflows/docker-build.yml) computes
git describe once and passes it as the APP_VERSION build-arg to the
Dockerfile. The Dockerfile's build-meta stage applies a precedence chain
(APP_VERSION > BUILD_TAG > RAILWAY_GIT_BRANCH > "dev") so a Railway
build — which cannot run git describe in its build sandbox — still produces
a meaningful, if less specific, value (the ref name) instead of a stale
hardcoded literal.
frontend/package.json version is demoted to 0.0.0 and is no longer the
source of any user-visible string. It exists only because pnpm requires a
SemVer there; the package is private: true and never publishes.
OpenAPI info.version keeps its existing semantics (TRA-672): it is the
spec version, bumped manually on breaking spec changes, currently 1.0.0.
It is intentionally not coupled to the platform version. Conflating the two
would couple every backend release to a spec-consumer breaking-change signal
and is rejected. This ADR records that separation explicitly so future
contributors do not "fix" the apparent inconsistency by re-coupling them.
API contract version (/api/v1/) is similarly unchanged and similarly
decoupled. Platform can ship v1.x → v2.x → v3.x without touching /api/v1/;
/api/v1/ flips to /api/v2/ only on a customer-visible breaking API change.
- Trip-test for AI ingestion partners:
/healthandinfo.versionare now both honest./healthreports a non-stale, machine-readable platform version.info.versionreports a spec semver, which is what generated clients expect. - No release-time file edits. A release is
git tag vX.Y.Z && git push --tags. CI rebuilds the image, the new version flows everywhere. No "did someone remember to bump?" failure mode.Inverted by ADR 0004. A release is now a file edit — one line, in a reviewed PR — and that is the point: the old form appeared in no diff and passed no checks. The "did someone remember to bump?" failure mode is answered by CI minting the tag from the file rather than by there being no file.
pnpm-lock.yamlmay re-resolve when the demoted0.0.0is first installed; harmless for a private package.- Local
pnpm devhas no build-time env, so the nav header readsdev. This is correct — there is no released artifact to name. - Railway preview cannot run
git describe, so its UI shows the branch name (e.g.worktree-tra-485-...) instead of a full describe string. This is an acceptable degradation for non-prod surfaces. Once prod images come from CI (GHCR), prod always has a full describe. - GKE move (TRA-351) is unaffected. The image is built once by CI with the platform version baked in; the deploy target (Railway vs GKE) only decides who pulls the image, not what is in it.
- Runtime fetch (
/api/v1/version): frontend asks backend for the version on render. Single-source, but adds a network round-trip for a value that is static for the lifetime of the bundle, needs a loading state, and depends on/api/v1/versionbeing reachable and unauthenticated when the header first renders (pre-auth, Device Status = Disconnected). Build-time injection avoids all of that. - release-please / standard-version / semantic-release: automates tag
cuts from Conventional Commits. Solves a coordination problem (multiple
contributors, divergent bump-level judgment) that this team does not have
at 1.5 FTE. Manual
git tag+ CHANGELOG.md edit is more honest about the team's actual coordination cost. Revisit at 5+ engineers. - Couple
info.versionto platform version: rejected.info.versionis a spec-consumer signal — it changes when generated SDKs need to regenerate. Coupling it to the platform release cadence would emit false-positive breaking-change signals on every backend bump.