# Transport to Web & Apps Overview

> How browser and mobile clients reach the voice stack over QUIC: WebTransport first, with a WebRTC codec tap and a clear statement of the fallback status.

A browser tab — or eventually a phone app — can place and answer calls,
stream mic audio up, and play agent audio back, with telephony-grade latency
and no SFU in the middle. This page maps how a client-side app reaches
FirstByte Voice: which transport carries the audio, how the SDK reuses the
browser's own encoder, and exactly what is shipped versus planned today.

The short version: **browser clients use QUIC directly.** Encoded Opus frames
travel over [MoQT](https://datatracker.ietf.org/wg/moq/about/) (Media-over-QUIC
Transport) over WebTransport, on a single `:443` connection to
`relay.firstbyte.fast`. This is the same track substrate as every other leg of
the call (`voice/<sid>/{uplink,downlink}`). There is no media SFU and no
gateway round-trip on the audio path.

## The transport ladder

The standing design is a **ladder**: QUIC/MoQT is first-class, with WebSocket
(control/data) and WebRTC (media) as the fallbacks for networks where UDP is
blocked or a browser lacks WebTransport. Know *where each rung ships today*.
The ladder is complete in the native SDK cores, but the browser/TypeScript SDK
is WebTransport-only right now.

| Rung | Carries | Browser / TS SDK | Native cores (Py/Go/Rust/Java/.NET) |
| --- | --- | --- | --- |
| **WebTransport + MoQT** (primary) | Encoded audio, all tracks | **Shipped** — the only media path | Shipped (QUIC, ALPN `h3`) |
| **WebSocket fallback** | MoQT muxed over WSS | Not yet — planned | **Shipped** — sticky auto-fallback rung |
| **WebRTC media leg** | Server-terminated SRTP audio | Not yet on this SDK path | Server-side termination exists |

> **NOTE:**
> On the browser today, if the QUIC/WebTransport path is unavailable, the SDK
> does **not** silently downgrade to WebSocket or WebRTC media. That rung is not
> yet shipped for the TypeScript client. The QUIC→WebSocket fallback ladder
> currently lives only in the native SDK cores. Plan browser deployments for a
> WebTransport-capable, UDP-reachable network. See
> [Browser compatibility](/modalities/voice/transport-web/browser-compatibility)
> for the gate and the current matrix.

## WebTransport: the browser-native QUIC path

WebTransport gives the browser a real QUIC connection; you run no UDP
plumbing. The SDK's `MoqtClient` dials `relay.firstbyte.fast`, negotiates the
`moqt-16` subprotocol, and returns immediately. It queues your publishes and
subscribes and replays them once the session is up. It then auto-reconnects
with capped backoff if the link drops. Audio then flows as MoQT objects on the
call's two tracks:

```
voice/<sid>/uplink     mic → cloud     (what the caller is saying)
voice/<sid>/downlink   cloud → caller  (what is played back)
```

"Uplink" and "downlink" are defined from the caller's point of view. Each
track carries a **capability** tag that the relay routes on. Thus an ASR
consumer, a recorder, or an AI agent can subscribe to a call's audio without
the publisher knowing they exist. That is the same wire model the server and
telephony legs use; see [Realtime tracks](/concepts/realtime-tracks). Full
setup, dev certificates via `serverCertificateHashes`, and the `127.0.0.1`
local-dev gotcha live in [WebTransport](/modalities/voice/transport-web/webtransport).

## Reusing the browser's audio pipeline (WebRTC diversion)

Browser voice sounds good because **you keep the browser's audio pipeline but
not its transport.** A loopback `RTCPeerConnection` runs the browser's capture
graph: echo cancellation, gain control, noise suppression, the Opus encoder,
and the jitter buffer. An
[`RTCRtpScriptTransform`](/modalities/voice/transport-web/rtcrtpscripttransform)
that runs in a Worker taps the **encoded** Opus frames and hands them to MoQT.
The WebRTC ICE/DTLS/SRTP transport is never used; only its codec and processing
are. Raw PCM never crosses the wire. Because the tap is at the encoded-frame
layer, the relay only ever sees opaque payloads.

  1. **Capture and encode**
`captureMicrophone(publication)` requests the mic with AEC/AGC/NS enabled,
    wires the loopback peer connection, and installs the Worker transform on
    the sender. The browser's Opus encoder then runs on your audio.
  2. **Publish over QUIC/MoQT**
The Worker reads each encoded frame off the transform's readable stream and
    writes it to `voice/<sid>/uplink` as a MoQT object over WebTransport. The
    relay fans it to whatever the engine bridged the call to — a SIP leg, an
    agent, a recorder.
  3. **Subscribe and play back**
Downlink Opus arrives on `voice/<sid>/downlink`. The SDK's `OpusPlayer`
    decodes it with WebCodecs and renders it through an audio-worklet ring
    buffer. The buffer pads silence on underrun.

```ts
import { MoqtClient, captureMicrophone, OpusPlayer } from "clutchcall_connect/moqt";

const client = await MoqtClient.connect("quic://relay.firstbyte.fast", {
  token,
  requireEncodedTransform: true, // refuse any non-tapped path
});

const uplink = client.publishAudio(`voice/${sid}/uplink`, "opus");
const mic = await captureMicrophone(uplink); // taps encoded Opus → MoQT

const player = new OpusPlayer();
client.subscribeAudio(`voice/${sid}/downlink`, (frame) => player.write(frame));
```

> **WARNING:**
> The encoded-frame tap is the **only** capture path. The legacy main-thread
> `createEncodedStreams()` branch was removed. If a browser lacks
> `RTCRtpScriptTransform`, `captureMicrophone` **throws**; it does not fall back
> to an untapped path. `MoqtClient.connect` also refuses when
> `requireEncodedTransform` is set. This is deliberate: it keeps the encoded-Opus
> guarantee (frame-level E2EE readiness) intact.

The one-call helper and its options are in
[One-line WebRTC diversion](/modalities/voice/transport-web/webrtc-diversion); the
capture internals and codec details are in
[Browser audio capture](/modalities/voice/transport-web/browser-audio-capture).

## Mobile apps

> **NOTE:**
> Native **iOS/Android** transport is **planned, not shipped.** There is no
> Swift or Kotlin SDK today. The planned path is a portable QUIC/MoQT client
> compiled through a shared C++ FFI (per-ABI `.so` on Android, static `.a` on
> iOS, and a `.jslib` shim for WebGL builds). To ship to phones now, run the app
> inside a WebTransport-capable mobile browser view, or reach the engine through
> a server-side leg. Track status in
> [Mobile app transport](/modalities/voice/transport-web/mobile-app).

## Where to go next

  - **[WebTransport](/modalities/voice/transport-web/webtransport)** — The browser-native QUIC path — connect, subprotocol, dev certs, and the
    track model in detail.
  - **[One-line WebRTC diversion](/modalities/voice/transport-web/webrtc-diversion)** — Divert an existing WebRTC audio pipeline onto QUIC with a single call.
  - **[RTCRtpScriptTransform](/modalities/voice/transport-web/rtcrtpscripttransform)** — How the encoded-Opus tap works in a Worker, and why it is the only path.
  - **[Browser audio capture](/modalities/voice/transport-web/browser-audio-capture)** — Capture, encode, and publish mic audio — options, codecs, and playback.
  - **[Browser compatibility](/modalities/voice/transport-web/browser-compatibility)** — What works where, the hard-gate, and the current fallback story.
  - **[Mobile app transport](/modalities/voice/transport-web/mobile-app)** — The planned native path for carrying voice from iOS and Android apps.
