Today Platform Web — Dev Docs
Architecture

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:

HeaderWeb valueWhy
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 routingweb-client clients enter a different auth-flow branch than macos-client or android-client
  • Version-gated features — backend features behind a "minimum client version" flag check X-App-Version against 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:

FileWhat it does
apps/web/src/hooks/use-api-client.tsThe main API client (React Query fetcher)
apps/web/src/hooks/use-session.tsThe session refresh / introspection client
apps/web/src/lib/chat/protocol/agent-client.tsThe 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:

  1. Cloud team raises a new gate (e.g. "min version 2.1.0 for feature X")
  2. Web bumps APP_VERSION_HEADER_VALUE to '2.1.0' in the same change set
  3. 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-client

Adding 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)
  • Auth interaction architecture — the auth-middleware layer that consumes X-Client-Platform
  • API codegen — how the generated @hey-api/client-fetch client wires in these headers via the client-config.ts

On this page