Mobile API Backward Compatibility
Ensuring our mobile users never break
The Problem
Current Architecture
┌─────────────────┐ ┌─────────────────┐
│ Mobile App │ │ Backend │
│ (Hero-payments │ ◄─────► │ mono/api │
│ /mobile) │ GQL │ │
└─────────────────┘ └─────────────────┘
│ │
▼ ▼
App Store GitHub
(users stuck (deploys
on old versions) instantly)
The Gap
| Mobile | Backend |
|---|---|
| Separate repo | mono repo |
| Release cycle: weeks | Release cycle: daily |
| Users control updates | We control deploys |
| Multiple versions in production | Single version |
What Can Go Wrong?
A backend developer removes or renames a field...
# Before
type MobileSigninOutput {
token: String!
refreshToken: String!
user: User!
}
# After (breaking change!)
type MobileSigninOutput {
accessToken: String! # renamed!
refreshToken: String!
user: User!
}
The Result
┌─────────────────┐
│ Mobile v3.1.0 │ ──► token ──► ❌ Field not found
│ (50% users) │
└─────────────────┘
┌─────────────────┐
│ Mobile v3.2.0 │ ──► accessToken ──► ✅ Works
│ (50% users) │
└─────────────────┘
50% of users have a broken app
Current Protections
| Protection | Catches Breaking Changes? |
|---|---|
| Schema snapshot test | ⚠️ Detects changes, not compatibility |
| Slack notification | ❌ Manual review only |
| Integration tests | ❌ Tests API, not mobile queries |
| Code review | ❌ Human error prone |
Real Risk Scenarios
- Field removal - Mobile queries fail
-
Type change -
Int→Stringbreaks parsing -
Nullability change -
String!→Stringcrashes app - Enum value removal - App can't handle response
- Argument change - Query rejected by server
Why This Is Critical
- No forced updates - Users stay on old versions for months
- App store review - Fixes take days to reach users
- Reputation damage - Broken app = bad reviews
- Support burden - "App doesn't work" tickets
Possible Solutions
Options Considered
| # | Solution | Approach |
|---|---|---|
| 1 | GraphQL Inspector | Validate operations against schema |
| 2 | Codegen Validator | TypeScript compilation check |
| 3 | Schema Contracts | Mobile defines required fields |
| 4 | Operation Registry | S3/npm published operations |
| 5 | Apollo Rover | Apollo Studio schema checks |
Why GraphQL Inspector?
| Criteria | Inspector | Codegen | Contracts | Registry | Rover |
|---|---|---|---|---|---|
| Setup effort | Low | Low | High | High | Medium |
| Accuracy | High | Medium | Medium | High | High |
| Error clarity | Excellent | Poor | Good | Good | Good |
| New dependencies | 1 npm | None | Tooling | S3+CI | SaaS |
| Maintenance | Low | Low | Medium | Medium | Low |
GraphQL Inspector Wins
- Purpose-built for breaking change detection
- Clear errors - shows exactly which operation breaks
-
Single dependency - just
@graphql-inspector/cli - No SaaS - runs locally in CI
- Battle-tested - used by Apollo, Guild, major companies
What It Catches
- Field removals
- Field renames
- Type changes (
Int→String) - Nullability changes (
String!→String) - Argument changes
- Enum value removals
- Deprecated field usage
Example Error Output
❌ Breaking change detected!
- Field "token" removed from "MobileSigninOutput"
+ Suggestion: Add @deprecated first, remove in 6 months
Affected operations:
└── mobileSignin (src/auth/signin.graphql:12)
Compare to codegen error:
TS2339: Property 'token' does not exist on type...
Recommended Solution
Goal
Block API deployments that break active mobile versions
Automatically. In CI. Before merge.
What We Have
Schema snapshot already exists:
apps/api/src/00_infra/graphql/
__snapshots__/
graphql.schema.unit.spec.ts.snap ◄── Full SDL schema
Mobile repo with version tags:
github.com/Hero-payments/mobile
tags: v3.2.0, v3.1.0, v3.0.0, ...
Proposed Solution
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ API PR │ │ Mobile Repo │ │ Config │
│ (schema │ │ (GraphQL │ │ (oldest │
│ snapshot) │ │ operations) │ │ version) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└────────────────────┼────────────────────┘
▼
┌─────────────────┐
│ GraphQL │
│ Inspector │
│ (validates) │
└────────┬────────┘
│
┌────────▼────────┐
│ ✅ Pass / ❌ Fail │
└─────────────────┘
How It Works
- PR opened on mono/api
- CI reads oldest supported version (from config - TBD)
- CI lists all tags from oldest to latest
- CI clones mobile repo at each version tag (parallel)
- GraphQL Inspector validates operations against schema
- CI fails if any operation is incompatible
CI Pipeline Addition
mobile_compatibility:
strategy:
matrix:
mobile-version: [3.2.0, 3.1.0, 3.0.0] # All tags >= oldest
steps:
- checkout api
- checkout mobile @ tag
- run: graphql-inspector validate \
mobile/operations.graphql \
api/schema.snapshot
Note: Oldest version location TBD (config file, env var, etc.)
Time Impact
| Step | Duration |
|---|---|
| Fetch active versions | ~5s |
| Checkout mobile (cached) | ~10s |
| Validate operations | ~15s |
| Total (parallel) | ~30s |
~1 minute added to PR CI (all versions in parallel)
Error Output Example
❌ Validation failed for mobile v3.1.0
✖ Field "token" was removed from "MobileSigninOutput"
Used in:
- src/auth/signin.graphql:12
- src/auth/refresh.graphql:8
Breaking for: mobileSignin query
Caching Strategy
- uses: actions/cache@v4
with:
path: mobile-repo
key: mobile-repo-base
- First run: ~30s (full clone)
- Cached runs: ~10s (fetch + tag checkout only)
Implementation Plan
-
Add
@graphql-inspector/cli -
New CI job in
api.pr.yml - Validate against oldest to latest mobile version
Questions
Open Questions
- Oldest version config - Where to store it? (env var, config file, repo secret?)
- Blocking vs warning - Hard fail or warn initially?
- Deprecation window - How long before field removal?
deck
By Remy Choffardet
deck
- 12