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

  1. Field removal - Mobile queries fail
  2. Type change - IntString breaks parsing
  3. Nullability change - String!String crashes app
  4. Enum value removal - App can't handle response
  5. 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

  1. Purpose-built for breaking change detection
  2. Clear errors - shows exactly which operation breaks
  3. Single dependency - just @graphql-inspector/cli
  4. No SaaS - runs locally in CI
  5. Battle-tested - used by Apollo, Guild, major companies

What It Catches

  • Field removals
  • Field renames
  • Type changes (IntString)
  • 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

  1. PR opened on mono/api
  2. CI reads oldest supported version (from config - TBD)
  3. CI lists all tags from oldest to latest
  4. CI clones mobile repo at each version tag (parallel)
  5. GraphQL Inspector validates operations against schema
  6. 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

  1. Oldest version config - Where to store it? (env var, config file, repo secret?)
  2. Blocking vs warning - Hard fail or warn initially?
  3. Deprecation window - How long before field removal?

deck

By Remy Choffardet