Skip to content

ADR 0066 — Mobile API /api/mobile/v1/* Contract (Wave-8)

Status Date Author Supersedes Superseded by
Accepted 2026-05-16 André Luiz Gallon

Context

ADR 0057 D4 mandates a NEW /api/mobile/v1/* backend namespace for the mobile companion app rather than reusing admin-console internal APIs.

This ADR formalizes the contract: authentication, payload shapes, versioning, rate limits, observability, and audit chain integration.

Architectural decision

10 LOCKED decisions.

D1: Path prefix /api/mobile/v1/* mounted on admin.tlsstress.art

Same FQDN as admin-console (no separate origin). Reasons:

  • Single TLS cert (ADR 0059 ACME-Everywhere)
  • Same Cloudflare edge + WAF rules
  • Same audit chain (cell-local Merkle)
  • Easier ops: one service to deploy

The router differentiates by path: /admin/* (web UI) vs /api/mobile/v1/* (mobile JSON API). Mobile API does NOT need session cookies (uses bearer tokens).

D2: Authentication = Auth0 JWT bearer + mobile session ID

Two-token model:

  • Auth0 JWT (RS256 signed, 1h TTL): proves Auth0 OIDC login succeeded. Audience claim mobile_app distinguishes from web client.
  • Mobile Session ID (server-issued, opaque, 30d TTL): paired with device fingerprint. Stored hardware-backed (D3 of ADR 0057). Used as refresh credential after JWT expiry.

Request flow:

Authorization: Bearer <auth0-jwt>
X-Mobile-Session-Id: <opaque>
X-Mobile-Device-Fingerprint: <SHA-256 of device-info>
X-Mobile-App-Version: 1.2.3

JWT expiry → 401 with WWW-Authenticate: Bearer error="expired". Mobile client uses Session ID to call /api/mobile/v1/auth/refresh, gets fresh JWT.

D3: Versioning = path-based (/v1/*)

No header negotiation. Major bumps create new path:

  • /api/mobile/v1/* current
  • /api/mobile/v2/* next major (breaking)

Minor changes additive only. Apps support last 2 major versions indefinitely (force-upgrade via EAS Update for security patches).

D4: Payload format = JSON + camelCase

  • application/json content-type
  • camelCase field names (matches TypeScript convention)
  • Mobile-specific denormalization (avoid N+1 mobile-side requests)
  • Image URLs include thumbnail variants (url, urlThumb, urlMedium, urlFull)
  • Timestamps ISO 8601 with explicit timezone (2026-05-16T13:00:00Z)

D5: Rate limits per device

Token bucket keyed by device_fingerprint:

Endpoint Burst Sustained
Auth (/auth/*) 5/min 1/sec
Read (/dashboard/*, /audit/*) 60/min 2/sec
Write (/tests/*, /approvals/*) 30/min 0.5/sec
Push registration 3/min 0.1/sec

Burst rate limit headers on every response:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1715865600

429 with Retry-After on exhaustion.

D6: Endpoint surface (P1 scope)

POST   /api/mobile/v1/auth/register                — device registration
POST   /api/mobile/v1/auth/exchange                — Auth0 JWT → session
POST   /api/mobile/v1/auth/refresh                 — session → fresh JWT
POST   /api/mobile/v1/auth/logout                  — revoke session

GET    /api/mobile/v1/dashboard/summary            — overall fleet health
GET    /api/mobile/v1/dashboard/alerts             — active alerts (paginated)

GET    /api/mobile/v1/tests                        — list (cursor-paginated)
GET    /api/mobile/v1/tests/{id}                   — detail
POST   /api/mobile/v1/tests                        — create
PATCH  /api/mobile/v1/tests/{id}                   — update (state mostly)

GET    /api/mobile/v1/audit-log                    — cursor-paginated
GET    /api/mobile/v1/audit-log/{id}               — detail

POST   /api/mobile/v1/push/register-token          — register Expo Push token
DELETE /api/mobile/v1/push/register-token          — unregister (logout)

GET    /api/mobile/v1/me                           — current user info
GET    /api/mobile/v1/me/permissions               — capability list

P2 (approvals + fleet view):

GET    /api/mobile/v1/approvals/inbox
POST   /api/mobile/v1/approvals/{id}/approve
POST   /api/mobile/v1/approvals/{id}/reject

GET    /api/mobile/v1/fleet/cells                  — geo-located cells
GET    /api/mobile/v1/fleet/cells/{id}/health

P3 (Watch + AR + widgets):

GET    /api/mobile/v1/watch/compact-summary        — sub-1KB payload for Watch
POST   /api/mobile/v1/ar/dc-inventory/verify       — AR attestation upload
GET    /api/mobile/v1/widgets/dashboard-glance     — Home Screen widget payload

D7: Cursor pagination (NOT offset)

All list endpoints use opaque cursor strings:

GET /api/mobile/v1/tests?cursor=eyJ0cyI6IjIwMjYtMDUtMTYifQ

Response:

{
  "items": [...],
  "nextCursor": "eyJ0cyI6IjIwMjYtMDUtMTUifQ",
  "hasMore": true
}

Server-side TTL: cursor valid 24h. Reasons: forward-only navigation fits mobile UX; allows infinite scroll without offset cost.

D8: Audit chain integration

Every mobile write emits an audit entry with mobile-specific fields:

{
  "eventType": "mobile.test.start",
  "actor": "user-id@example.com",
  "deviceFingerprint": "sha256:...",
  "appVersion": "1.2.3",
  "platform": "ios",
  "platformVersion": "17.4",
  "ipAddress": "203.0.113.42",
  "geolocation": null,  // not collected (privacy)
  "payload": { ... },
  "hash": "..."
}

geolocation always null — mobile app does NOT request location permission unless user explicitly opts in for fleet map (P2).

D9: Observability

Every mobile request tagged with:

  • app_version label (Prometheus + ClickHouse)
  • platform label (ios / android)
  • os_version label (e.g., 17.4, 14)
  • device_class label (iphone/ipad/android-phone/android-tablet)

Metrics emitted (canonical names following pkg/oobi/satellite/metrics/ pattern):

tlsstress_mobile_api_requests_total{endpoint, status, app_version, platform}
tlsstress_mobile_api_request_duration_seconds{...}
tlsstress_mobile_auth_refreshes_total{outcome}
tlsstress_mobile_push_deliveries_total{category, outcome}

D10: Error response format = RFC 7807 Problem Details

{
  "type": "https://docs.tlsstress.art/errors/test-not-found",
  "title": "Test not found",
  "status": 404,
  "detail": "Test 01HXXX not found in deployment 01HYYY",
  "instance": "/api/mobile/v1/tests/01HXXX"
}

Mobile client SDK auto-translates type URLs into localized user-facing messages (5 i18n locales per Wave-2: en/pt-BR/es/de/ja).

Trigger gates (BLOCKING — same as ADR 0057 D BLOCKING list)

This API ships when ADR 0057's trigger gates are met. Until then, endpoints live behind feature flag MOBILE_API_ENABLED=false.

Backwards compatibility

Existing admin-console internal APIs are NOT impacted. Mobile API is a new namespace. Server code can share business logic via internal service layer; mobile API handlers are thin adapters.

Closes audit gap

Gap #25 — Mobile API contract not formalized; review board had no standard for mobile-specific payload + auth + audit integration

Cross-references

  • ADR 0057 — Wave-8 umbrella (sibling)
  • ADR 0054 — PQC TLS preserved
  • ADR 0058 — dual-stack
  • ADR 0059 — same cert
  • ADR 0055 — admin console architecture
  • RFC 7807 Problem Details for HTTP APIs
  • Auth0 RN Quickstart
  • Expo Push Notifications