Compatibility matrix
Which API versions support which clients.
The web app updates instantly. Mobile takes a week to be on most users' devices. Desktop's a slow trickle. Your API has to support all three versions simultaneously. The compatibility matrix is how you keep track.
The problem in one paragraph
You ship the API at v1.0. Web is at v1.0. You add a field, deploy API + web to v1.1 together — fine, they updated in lockstep. Now mobile pushes a v1.1 build to TestFlight, takes 24h to review, another week for the user to update. During that week, the user is on mobile v1.0 + API v1.1. Did you BREAK them, or are they FINE?
The matrix — write it down
Maintain a real document. docs/compat-matrix.md at the repo root:
# Client / API compatibility| API ver | Web ver | Mobile ver | Desktop ver | Notes ||---------|----------|------------|-------------|------------------------|| 1.0 | >= 1.0 | >= 1.0 | >= 1.0 | initial release || 1.1 | >= 1.0 | >= 1.0 | >= 1.0 | added optional field || 1.2 | >= 1.2 | >= 1.0 | >= 1.0 | web-only feature flag || 2.0 | >= 2.0 | >= 1.9 | >= 1.9 | BREAKING — see below |## 2.0 breaking changes- Removed deprecated /api/legacy-search- Renamed Product.code → Product.sku (was deprecated since 1.5)- Clients must send X-Client-Version header
Three things this gives you:
- A single source of truth before deploys.
- A way to refuse old clients (via X-Client-Version + middleware).
- A historical record when debugging "why did v1.2 break for some users?"
Two API rules that make compat easy
1. Adding fields is fine. Removing them is breaking.
Old clients ignore new fields. So adding is_featured: bool to /api/products does not break mobile v1.0 — it just doesn't use the field. Removing or renaming a field breaks every client that reads it.
2. New endpoints are fine. Changed semantics are breaking.
A new POST /api/search endpoint costs nothing — old clients call the old /api/products?q=. But if you change what POST /api/products returns (now it's 202 instead of 201), old clients break.
X-Client-Version — the runtime check
const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION // baked at build timeexport async function api(path: string, opts?: RequestInit) {return fetch(API_URL + path, {...opts,headers: {...opts?.headers,'X-Client-Version': APP_VERSION,'X-Client-Surface': 'web', // or 'mobile' / 'desktop'},})}
func VersionGate(minVersions map[string]string) gin.HandlerFunc {return func(c *gin.Context) {surface := c.GetHeader("X-Client-Surface")version := c.GetHeader("X-Client-Version")min, ok := minVersions[surface]if ok && semver.Compare("v"+version, "v"+min) < 0 {c.AbortWithStatusJSON(426, gin.H{"error": "client_too_old","min_version": min,"upgrade_url": "https://app.example.com/upgrade",})return}c.Next()}}
The HTTP 426 ("Upgrade Required") is the standard status code for "your client is too old". Each surface handles it differently: web reloads; mobile shows a forced-update screen; desktop triggers the auto-updater.
Per-surface upgrade UX
- Web: on 426, show a banner "a new version is available, refresh". The next page load gets the new JS. Easiest.
- Desktop: on 426, trigger the auto-updater (from the Desktop course). User clicks "Update", app restarts in < 30s.
- Mobile: hardest. You can show a forced screen with a link to the App Store, but if the app needs review, the user is stuck for hours. Plan ahead: keep mobile backwards-compat for at least 2-3 minor versions.
Real example — adding a required field
Marketing wants every product to have a category. How to roll it out without breaking mobile?
- API v1.5: Add the
categoryfield as optional. Default to empty string. Existing rows backfilled to empty. - Web + admin v1.5: Adds a UI to set it on new products.
- Mobile v1.5: Reads it if present, hides the section if empty.
- API v1.6 (months later): Make it required server-side. By now all active products have a category.
Three releases over weeks. Slow, but no client ever 500'd.
Quick check
Try it
Set up a real compatibility gate:
- Create
docs/compat-matrix.mdin your repo with the current versions. - Add
X-Client-Version+X-Client-Surfaceto every API call from web / admin / mobile / desktop. - Wire the VersionGate middleware in the API. Set the current min versions in a map.
- Hand-set web's version to v0.0.1 in
.env. Hit the API. Should get a 426 + upgrade JSON. - Implement the "new version available" banner on web that shows when a 426 comes back.
What's next
Final lesson — Staggered releases. With the compat matrix in hand, the actual deploy order: API first, then web, then desktop, then mobile (with reasoning at each step).
Spot a typo? Have an idea?
Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled — suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.
Suggest an improvement on GitHub