Cross-platform headers
X-Client-Platform and X-App-Version are sent on every API request from web, macOS, and Android. The cloud server treats web-client as a first-class platform — no special-casing needed.
Every API request from apps/web carries two identifying headers:
| Header | Web value | Why |
|---|---|---|
X-Client-Platform | 'web-client' | Tells the cloud auth middleware which platform this is |
X-App-Version | '2.0.0' | Lets cloud gates trigger on specific client versions |
Values live in
apps/web/src/lib/api/platform-constants.ts:
export const APP_VERSION_HEADER_VALUE = '2.0.0'
export const CLIENT_PLATFORM_HEADER_VALUE = 'web-client'Why the contract is symmetric
macOS, Android, and the web app all send the same two headers with platform-specific values. The cloud server uses them for:
- Auth middleware platform routing —
web-clientclients enter a different auth-flow branch thanmacos-clientorandroid-client - Version-gated features — backend features behind a "minimum client
version" flag check
X-App-Versionagainst the gate value. Older clients see the feature disabled. - Telemetry segmentation — every analytics event tagged with platform
- version, no inference needed
The cloud server treats web-client as a first-class platform — there's
no special-casing, no if (platform === 'web') { ... } branches.
Nothing on the server needs to change when adding new web surfaces —
new routes, new pages, new BFF endpoints all inherit the same header
contract via the shared HTTP clients.
Where the headers are injected
Three HTTP entry points in apps/web consume the constants:
| File | What it does |
|---|---|
apps/web/src/hooks/use-api-client.ts | The main API client (React Query fetcher) |
apps/web/src/hooks/use-session.ts | The session refresh / introspection client |
apps/web/src/lib/chat/protocol/agent-client.ts | The agent SSE / message client |
Each one passes both headers on every request:
fetch(url, {
headers: {
'X-Client-Platform': CLIENT_PLATFORM_HEADER_VALUE,
'X-App-Version': APP_VERSION_HEADER_VALUE,
// ...
},
})Version-bump policy
The current X-App-Version value is '2.0.0'. The cloud's
CHARACTERS_STEP_MIN_VERSION = '1.1.0' gate is comfortably below it, so
the web client passes all current gates.
Bump APP_VERSION_HEADER_VALUE only when a future cloud gate requires
it. Random pre-emptive bumps create churn for no benefit. The pattern:
- Cloud team raises a new gate (e.g. "min version
2.1.0for feature X") - Web bumps
APP_VERSION_HEADER_VALUEto'2.1.0'in the same change set - PR title makes the cloud-gate dependency explicit so the cloud-side rollout sequencer knows to ship its side first
What 'web-client' means in the cloud's closed enum
The cloud auth middleware enforces a closed enum for X-Client-Platform:
unknown values are rejected with 401. The accepted values are:
web-client
macos-client
android-client
ios-client
windows-clientAdding a new platform string requires a coordinated server-side change
first. For now apps/web always sends web-client; apps/admin could
plausibly want its own value (admin-client) eventually, but at the moment
it sends the same web-client and is differentiated server-side by route
(/admin/* vs /v1/*) rather than by platform header.
What happens without the headers
A missing or wrong X-Client-Platform returns 401 from the cloud auth
middleware before any business logic runs. A missing or below-gate
X-App-Version returns 426 Upgrade Required for gated routes (the cloud
sends a JSON body explaining which gate failed; clients render an "update
the app" prompt).
Both behaviors are best understood by inspecting a captured failure rather than reading server code; the cloud team's docs have the exact response shapes.
Why constants in their own file
platform-constants.ts is a leaf file — no imports, just two export const
declarations. This means:
- Every HTTP-client module can import it without circular-dep risk
- A version bump is a one-line PR with maximum confidence
- The mock for unit tests is trivial (or unnecessary — the constants are benign values, not stateful infrastructure)
Related
- Auth interaction architecture — the auth-middleware
layer that consumes
X-Client-Platform - API codegen — how the generated
@hey-api/client-fetchclient wires in these headers via theclient-config.ts
OpalShaderShell
Three auth surfaces (`/login`, `/oauth/select-account`, `/oauth/consent`) share one shell that bundles the Opal background, the centered content column, and the beacon plumbing the brand mark needs to paint.
API codegen
TypeScript clients are generated from the backend's OpenAPI spec via `@hey-api/openapi-ts`. `pnpm api:generate` writes to `apps/web/src/lib/api/generated/` — never edit those files by hand.