25
Backend
04
Security
08
iOS
07
Infra

Check the GraphQL schema against a committed compatibility floor

ADR-0080 ACCEPTED · 2026-08-23
Check the GraphQL schema against a committed compatibility floor

Absorbs ADR-0008.

Decision

Only add to the GraphQL schema. Never remove or narrow a field a client might select, and never give an existing field a new required argument. New capability arrives as a new field beside the old one. Retiring a field means marking it @deprecated and continuing to serve it: deprecation tells the author of the next client what to avoid and does not license the server to stop answering.

Enforce the rule with a committed floor. The oldest schema we still support lives in the repository as an SDL file, and the build fails unless the current exported schema is a superset of it. The check runs with the flake checks that gate a push, because a compatibility break is a mistake the Rust compiler cannot see.

Any narrowing of the supported window is an edit to the floor file. That puts the decision in a diff of its own instead of making it a side effect of an unrelated change. Once the app is on people's phones, that edit strands every install that hasn't updated.

Why

A web frontend deploys with its backend. An iOS app updates when the phone's owner lets it: people defer updates for weeks, and App Store review adds days even to an urgent fix. We deploy the backend whenever we choose, so the set of schemas that installed apps were built against is always wider than the one the server serves, and a narrowing change cannot be undone for installs that already happened. GraphQL clients select fields individually, so an additive schema keeps every installed app working with no extra infrastructure.

The additive rule started as prose that nothing enforced, so it read like a guarantee and guaranteed nothing. The check answers what we have decided to keep serving. That is a policy, so it lives in a file someone edits on purpose.

Rejected alternatives

  • Consumer-driven contract testing with Pact. We ran a broker and a Rust provider test passed, but the Swift Pact libraries didn't work with current Xcode. The requirement was also narrower than a contract: the server must never break a query an installed client already sends.
  • Diffing against the live production endpoint, or against a release tag. Both report what the schema was. The floor should move when someone decides to drop a build, and never on every commit or because a rollback changed what production serves. A live endpoint would also turn an outage into a failed build.
  • A floor built from a manifest of the operations shipped apps send. GraphQL document validation never inspects output nullability or leaf scalar types. A field narrowed from String! to String still validates against every document a client can send, and it breaks the Apollo-generated Swift, where the non-null field became a non-optional property. The whole-schema floor catches that case.

Consequences

Backend deploys never strand an installed app, and backend and app releases need no coordination. Superseded fields accumulate until someone retires one, and retiring one is a decision about which installs to leave behind.

Nothing that reads only the current tree can tell a deliberate floor edit from a wholesale replacement of the file or a deletion of just the failing lines. Review has to notice the floor diff.

The check has to be right in both directions. One that rejects harmless changes forces the floor forward for changes that break nothing, and a floor moved casually stops meaning anything. Every run therefore proves itself on fixed controls: it must reject a breaking schema for each kind of break it claims to detect, accept a purely additive one, and fail if its comparison loop examined nothing.