---
title: Transports
description: "One client, three wires: one HTTP route, split routes for per-verb timeouts, or one WebSocket."
---

## One wire seam, same client

`api` picks the wire:

```ts
// in a 'use client' session module:
api: '/api/order-events'
api: (sessionId) => `/api/orders/${encodeURIComponent(sessionId)}/events`
api: { type: 'http', push: '/api/order-push', stream: '/api/order-stream' }
api: { type: 'ws', url: '/api/order-events' }
```

- **One route** is the default and the recommendation: a fixed URL or a
  session URL function, serving GET (SSE stream and history) and POST (push).
- **`http` split** serves the two verbs from separate routes, for platforms
  whose duration limits differ per verb.
- **`ws`** carries streams, pushes, and presence over one socket per session.
  A socket is one connection in both directions, so it has no split form.

The server side of each lives on [Routes](/server/routes). Everything above
the wire is shared: `useSession`, push acks and `confirmed`, presence
cadence, connection status, stall detection, and reconnection. SSE and
WebSocket resume from the same durable frontier and treat presence as
ephemeral, repaintable state.

## Session URL functions

Every session-scoped URL may be a `(sessionId) => string` function. A2
resolves it for each request or reconnect, then adds its standard query
parameters. Encode the ID when placing it in a path segment.

The function chooses a route, not authority. If a path or connection token is
scoped to one session, [`authorize`](/server/routes#the-http-route) must
compare that scope to `operation.sessionId`. Bind URL tokens to the same
session and keep them short-lived, because URLs can appear in infrastructure
logs.

## WebSocket

```ts app/orders/[orderId]/session.ts
'use client'
import { createClient } from 'experimental-a2/client'
import { createReact } from 'experimental-a2/react'
import { orders } from '@/contracts'

export const ordersClient = createClient({
  contract: orders,
  api: { type: 'ws', url: '/api/order-events' },
})
export const { SessionProvider, useSession } = createReact({
  client: ordersClient,
})
```

To share one socket across the client's sessions, set `multiplex: true` in
`api` and `multiplexWebSocket: true` on the
[route](/server/routes#the-websocket-route). A multiplexed URL is one plain
string, since the connection carries more than one session.

`ws` has no history lane. [`loadHistory`](/client/react#events-and-history)
is a bounded cold read over plain HTTP; on a `ws` api it throws a
`TypeError`.

## Lifecycle

When a socket closes, the client reconnects at its current frontier per its
reconnection policy, receives a fresh presence snapshot, and re-sends its own
latest presence. A multiplexed socket re-subscribes every active session at
its own frontier. Pushes keep their client-generated ids, so retries stay
idempotent.

The `createClient({ reconnect })` policy returns a delay, or `false` to
pause. The default reconnects with backoff indefinitely. A clean close also
runs the policy, with `error: null`. A paused session keeps its state and
leases; `session.reconnect()` resumes it.

Upgrade authorization stays in the
[HTTP route](/server/routes#the-websocket-route). Browser WebSockets hide a
rejected upgrade's status and body, so it surfaces as an opaque
`CONNECTION_FAILED` error. Structured subscription errors arrive once a
multiplexed socket is open.
