# WebTransport

> The browser-native QUIC path for voice media and data: one session on :443, MoQT riding it over streams, and the dev cert hash.

WebTransport is how a browser reaches the voice stack over raw QUIC. It gives
you a single bidirectional session to the relay, with no TCP, no WebSocket
framing, and no separate signalling socket. Every voice track, control message,
and data stream for a call rides inside that session. You open one session and
publish or subscribe. The transport handles the rest.

The stack terminates **QUIC, HTTP/3, and WebTransport on one `:443`**. There is
no second media port to open, no UDP range to whitelist beyond 443, and no raw
protocol port to reason about. The browser dials `https://relay.firstbyte.fast`
and gets a WebTransport session on the same endpoint that serves everything
else.

## Open a session

You never touch the `WebTransport` object directly. Every SDK ships a
`MoqtClient` that opens the session, negotiates the protocol version, and keeps
it alive across drops. Call `connect` and start publishing.

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

    const client = await MoqtClient.connect(
      "quic://relay.firstbyte.fast",
      workspaceToken,
      (state, reason) => console.log("transport", state, reason ?? ""),
    );
    // client is live — publish or subscribe voice/<sid>/{uplink,downlink}
    ```
  
  
    ```python
    from clutchcall.moqt import MoqtClient

    client = MoqtClient.connect(
        "quic://relay.firstbyte.fast",
        token=workspace_token,
        on_state=lambda state, reason=None: print("transport", state, reason),
    )
    ```
  

A few things happen under the hood that you can rely on:

- The SDK rewrites the `quic://` URL to `https://` and attaches the tenant
  token as a query parameter. The relay's control plane checks the token
  before any track flows.
- The client advertises the MoQT version as the WebTransport **subprotocol**
  (`moqt-16`). The relay refuses a session that does not offer it, so this is
  not optional. The SDK always sends it for you.
- `connect` returns as soon as the session is up. The client
  **auto-reconnects** with capped backoff if the link drops, and it replays
  every publication and subscription. The `onState` callback reports
  `Connecting` / `Connected` / `Reconnecting` / `Closed` / `Failed`.

> **NOTE:**
> The browser SDK is **WebTransport-only today**. If QUIC is blocked (some
> corporate proxies, UDP-filtered networks), the browser client cannot fall back
> on its own. The QUIC→WebSocket fallback ladder currently ships only in the
> native SDK cores. For those environments, use the WebRTC media leg described in
> [Browser compatibility](/modalities/voice/transport-web/browser-compatibility).

## How MoQT rides the session

[MoQT](https://datatracker.ietf.org/wg/moq/about/) (Media-over-QUIC Transport)
carries your named tracks (`voice/<sid>/uplink` and
`voice/<sid>/downlink` for a call) inside the one WebTransport session. It uses
two of WebTransport's primitives:

- **A control stream** — a single bidirectional stream carries MoQT setup and the
  subscribe/publish signalling. This is where the client and relay agree on
  versions, announce namespaces, and track subscriptions.
- **Object streams** — each track group opens its own **unidirectional stream**
  of objects. Audio rolls over to a fresh group roughly once a second, so a
  live call is a rolling series of short-lived streams, each carrying ~1 s of
  encoded audio objects. Each group on its own stream means a lost or
  slow group cannot head-of-line-block the next one. The transport keeps
  moving.

You do not manage streams by hand. You publish objects to a track and subscribe
to a track by name. The SDK maps that onto the streams above. See
[Sessions, calls, tracks & streams](/modalities/voice/concepts/sessions-calls-tracks-streams)
for the track model.

## Streams vs datagrams

WebTransport offers two ways to move bytes, and they trade reliability for
latency differently:

| Primitive | Delivery | Ordering | Voice uses it for |
| --- | --- | --- | --- |
| **Streams** | Reliable — retransmitted until delivered | Ordered within a stream | **All voice media today.** Each track group is one stream, so audio objects arrive in order and complete, while separate groups stay independent. |
| **Datagrams** | Unreliable — sent once, may be dropped | Unordered | The browser voice path does not use datagrams. They are a WebTransport capability that MoQT can carry single-object payloads over, reserved for latency-critical, loss-tolerant data. |

> **TIP:**
> The per-group stream design gives you the best of both: **reliable, in-order
> audio within the ~1 s a group lives**, without one bad group stalling the whole
> call. You get datagram-like isolation between groups and stream reliability
> inside them. You write no datagram loss handling yourself.

## The server certificate hash

In production, the relay presents a normal CA-issued certificate with
**multi-brand SNI**, so the browser trusts it like any HTTPS origin. You pass
no hash, and the `serverCertificateHashes` machinery never engages.

For **local development** against a self-signed relay, WebTransport lets you
pin the exact certificate by its SHA-256 fingerprint, so you do not need to
install a CA. Pass the base64 hash, and the SDK wires it into the session:

  
    ```ts
    const client = await MoqtClient.connect(
      "quic://127.0.0.1:4443",
      devToken,
      undefined,
      { serverCertificateHash: "Base64Sha256OfTheCert=" },
    );
    ```
  
  
    ```ts
    // No hash — the relay's public certificate is trusted normally.
    const client = await MoqtClient.connect(
      "quic://relay.firstbyte.fast",
      workspaceToken,
    );
    ```
  

> **WARNING:**
> The certificate-hash path has strict rules the browser enforces:
> 
> - The certificate must be **ECDSA** (not RSA) with a validity window of
>   **14 days or less**. Regenerate it on that cadence for local dev.
> - Dial **`127.0.0.1`**, not `::1`. The IPv6 loopback breaks the WebTransport
>   handshake.
> - It only covers self-signed dev relays. Never ship a pinned hash to
>   production. Use a real certificate.

## Related

  - **[Transport to web & apps](/modalities/voice/transport-web/overview)** — How browser and mobile clients reach the voice stack over QUIC.
  - **[Browser audio capture](/modalities/voice/transport-web/browser-audio-capture)** — Capture and encode the microphone, then publish it over this session.
  - **[One-line WebRTC diversion](/modalities/voice/transport-web/webrtc-diversion)** — Divert an existing WebRTC audio pipeline onto QUIC in one call.
  - **[Browser compatibility](/modalities/voice/transport-web/browser-compatibility)** — What works where, and the fallback when QUIC is blocked.
