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_appdistinguishes 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/jsoncontent-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_versionlabel (Prometheus + ClickHouse)platformlabel (ios/android)os_versionlabel (e.g.,17.4,14)device_classlabel (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