# From Self-Hosted LiveKit

> Move a self-hosted LiveKit voice deployment onto QUIC transport. Install the transport plugin in your agent, put us in front of your room, or swap one import in the browser.

You run a self-hosted LiveKit stack: an SFU that you operate, agents built on
the LiveKit runtime, ICE/DTLS/SRTP to negotiate, and a media server to keep
alive. This page moves that workload onto FirstByte **without a rewrite of
your agent logic**. You keep your models, your prompts, and your conversation
loop. You drop the SFU and the transport problems.

There are three ways in. They solve different problems:

  - **Transport plugin** — **Recommended.** Install a package into your `livekit-agents` worker. Your
    agent runs verbatim over our QUIC transport. You retire the SFU
    completely.
  - **Transport in front** — Keep your LiveKit **room** in operation. Bridge the media into it with a
    sidecar. Best when other participants still join the room.
  - **Drop-in client shim** — Swap one import in your **browser** app. Your `Room` / `RoomEvent` / track
    code still compiles, but runs over our transport instead of your SFU.

> **TIP:**
> Paths 1 and 2 differ in what you **retire**. The plugin removes your LiveKit
>   server from the picture entirely. The front-proxy keeps it and puts us in
>   front. You can also give us the conversation loop and bring only your models.
>   For that, see
>   [Keep Your Existing Runtime](/modalities/voice/runtime/keep-existing).

## Path 1 — install the transport plugin (recommended)

The LiveKit Agents SDK skips its own room/WebRTC layer whenever a session
already has custom audio IO set. This is the same seam that LiveKit uses for
its TCP console mode. The transport plugin binds that seam to a
QUIC/WebTransport connection to FirstByte. Thus your `entrypoint` runs
**byte-for-byte unchanged**. There is no room, no SFU, and no LiveKit server
anywhere in the path.

```
caller ──SIP/QUIC──▶  FirstByte transport  ──QUIC datagrams──▶  your livekit-agents worker
   (PSTN or browser)   (single :443 plane)      (agent dials out)      (unchanged + plugin)
```

  1. **Install the plugin next to your agent**

      
```bash
pip install "livekit-plugins-clutchcall[quic]"
```
      
      
```bash
npm i @clutchcall/livekit-transport @fails-components/webtransport
```
      
    
  2. **Swap your worker runner**
Replace `cli.run_app(...)` with `run_agent_worker(...)`. Pass your existing
    `entrypoint`. The worker dials out to `:443`, registers under an agent
    name, and serves the calls that the engine assigns. It reconnects
    automatically if the link drops. This is the only new file in your repo.
    The agent itself does not change.

    The full worker snippet for both languages is in
    [Run a LiveKit Agent on FirstByte Transport](/modalities/voice/recipes/livekit-transport).
  3. **Point your inbound numbers at us**
Move the trunks that feed your LiveKit agent onto FirstByte. Then attach
    an agent whose entry node is a `VENDOR_BRIDGE` with
    `vendor_provider: "clutchcall"` and `vendor_room` set to the name
    that your worker registered under.

    See [SIP Trunking](/modalities/voice/transport-telephony/sip-trunking) and
    [Number Provisioning](/modalities/voice/transport-telephony/number-provisioning).
  4. **Cut over and retire the SFU**
Move numbers in waves. When traffic is stable, your LiveKit server carries
    no voice traffic at all. Shut it down, along with the TURN relays and the
    ICE plumbing.

> **WARNING:**
> **Check two things before you commit to this path.** The worker needs
>   outbound **UDP/443** for media (the WebSocket fallback is presence-only). The
>   worker also runs in **register-and-wait** mode: a long-lived process, not a
>   scale-to-zero serverless function. If either is a problem, take Path 2.

## Path 2 — put the transport in front of your agent

Take this path when your LiveKit **room** must stay in operation — other
participants join it, or something else in your stack depends on it. Your
LiveKit agent, room logic, and model plumbing stay **byte-for-byte the same**.
What changes is the media plane in front of them. This path also covers the two
cases that the plugin does not yet: a scale-to-zero serverless worker, and a
network with no outbound UDP. Callers (PSTN via SIP trunks, or browsers) land
on FirstByte's single `:443` QUIC/WebTransport plane. A lightweight **bridge
sidecar** that you run next to your LiveKit deployment carries the audio
between our transport and your LiveKit room. The runtime routes a call to that
bridge with a first-class **vendor-bridge** step. The runtime does not drive
the conversation itself.

```
caller ──SIP/QUIC──▶  FirstByte transport  ──vendor bridge──▶  LiveKit room ──▶ your agent
   (PSTN or browser)   (single :443 plane)        (sidecar)          (unchanged)
```

> **NOTE:**
> The vendor-bridge routing is a first-class part of the runtime. Its targets
>   are `livekit`, `vapi`, and `twilio`. The **bridge sidecar itself is a
>   separate component that you deploy** next to your LiveKit stack. It is not
>   bundled into the engine. Its exact packaging depends on your environment.
>   Unlike Path 1, this path keeps your LiveKit server in the call flow.

  1. **Point your inbound numbers at our transport**
Move the trunks that feed your LiveKit agent onto FirstByte. Your
    carrier trunk terminates on our SIP gateway. Numbers are provisioned per
    tenant under `<workspace-id>.sip.firstbyte.fast`. Nothing about your agent
    changes. Only the place where the call first lands changes.

    See [SIP Trunking](/modalities/voice/transport-telephony/sip-trunking) and
    [Number Provisioning](/modalities/voice/transport-telephony/number-provisioning).
  2. **Run the LiveKit bridge sidecar**
Deploy the bridge sidecar next to your LiveKit server. It connects to our
    transport on one side and to your LiveKit room on the other. It uses your
    existing LiveKit URL and access-token issuance. Audio moves across as Opus.
    Your agent sees a normal LiveKit participant.
  3. **Route calls to the bridge**
In the agent configuration for the number, select the **vendor bridge**
    target instead of the built-in runtime. As a concept, the agent config
    carries a vendor-bridge block that names LiveKit as the destination:

    ```json
    {
      "entry_node": "vendor_bridge",
      "vendor_bridge": {
        "vendor": "livekit",
        "room": "support",
        "url": "wss://your-livekit-host.example.com"
      }
    }
    ```

    When a call matches, the runtime gives its media to the bridge. It does not
    run a conversation DAG. Your LiveKit agent joins the room and answers as it
    always did.
  4. **Cut over and validate**
Move numbers over in waves. On each wave, check that the call reaches your
    LiveKit agent. Check that audio is two-way. Check that your existing
    telemetry still fires. The agent is unchanged, so the only new surface is
    the transport and the bridge. Validate those, not your model.

> **TIP:**
> This path is a step, not a dead end. When traffic is stable on our transport,
>   you can move to **Path 1**: install the plugin, then drop the sidecar and the
>   room. Or you can migrate the runtime itself. Swap the vendor bridge for our
>   managed runtime with your own models
>   ([BYO ASR/LLM/TTS](/modalities/voice/runtime/byo-asr-llm-tts) or
>   [BYO Speech-to-Speech](/modalities/voice/runtime/byo-speech-to-speech)).
>   Then retire the LiveKit stack when you are ready.

## Path 3 — the drop-in client shim (Preview)

> **WARNING:**
> **Preview / early access.** The `livekit-compat` shim covers the core browser
>   **voice** path: publish the mic, subscribe to remote audio, exchange data
>   messages. But it is not yet a drop-in for every `livekit-client` API. Video,
>   screenshare, and per-participant E2EE key rotation are deferred. A
>   session-setup race can occasionally deliver zero frames on the first connect
>   (retry). Pin a version. Test your flows. For a stable transport-only path,
>   use the raw MoQT primitives in
>   [Keep Your Existing Runtime](/modalities/voice/runtime/keep-existing).

Your LiveKit usage can be a **browser** app that talks to a room. For that
case, the shim exposes a `livekit-client`-compatible surface (`Room`,
`RoomEvent`, `createLocalAudioTrack`, participants, tracks) mapped onto our
MoQT/QUIC transport. For the common voice path, the migration is a one-line
import swap. There is no SFU, no ICE, and no media server.

  1. **Install the shim**
```bash
    npm install @clutchcall/livekit-compat
    ```
  2. **Swap the import**
Point your existing `livekit-client` imports at the shim. Your component
    code does not change in any other way.

    ```diff
    - import { Room, RoomEvent, createLocalAudioTrack } from "livekit-client";
    + import { Room, RoomEvent, createLocalAudioTrack } from "@clutchcall/livekit-compat";
    ```
  3. **Connect to the relay**
Your call sites stay the same. The shim reads room and identity from your
    access token. It opens a MoQT session on `:443` instead of a join to an
    SFU.

    ```ts
    const room = new Room();
    await room.connect(url, token);              // your relay.firstbyte.fast endpoint
    await room.localParticipant.setMicrophoneEnabled(true);
    room.on(RoomEvent.TrackSubscribed, (track) => track.attach());
    ```

### How the shim maps LiveKit concepts

The shim keeps LiveKit semantics but replaces the media plane entirely. There
is no SFU and no ICE. WebRTC's transport is never used. The mic still runs
through the browser's Opus encoder. But the shim taps only the **encoded
frames** and sends them over MoQT. See
[One-Line WebRTC Diversion](/modalities/voice/transport-web/webrtc-diversion).

| LiveKit concept | Runs as |
| --- | --- |
| `Room.connect(url, token)` | A MoQT session over WebTransport on `:443` |
| Participant identity | A room-scoped MoQT namespace (relay enforces isolation via the token) |
| Microphone track | Encoded Opus frames published as MoQT objects |
| Remote audio track | A MoQT subscription decoded with WebCodecs into a Web Audio graph |
| `publishData` / `DataReceived` | A reliable MoQT data track |

> **WARNING:**
> `room.connect()` **refuses to proceed** unless the browser supports encoded
>   media transforms (`RTCRtpScriptTransform`). It fails loudly. It does not fall
>   back silently to plaintext WebRTC. Today that means Chromium-family browsers.
>   Also, the browser client is WebTransport-only (no WebSocket fallback leg
>   yet). See
>   [Browser Compatibility](/modalities/voice/transport-web/browser-compatibility).

## What you gain, and what stays the same

  **What you drop**

You drop the SFU that you operate, the ICE/DTLS/SRTP negotiation, the TURN
    relays for blocked networks, and the media server's scale and on-call
    burden. Media moves on one QUIC/WebTransport plane on `:443`.

  **What stays yours**

You keep your agent logic, prompts, and models. On Path 1, your
    `entrypoint` and `AgentSession` run verbatim. On Path 2, your LiveKit room
    code is unchanged. On Path 3, your browser component keeps its
    `Room`/`RoomEvent` shape. We move frames. We do not impose a runtime.

  **Credentials**

Paths 2 and 3 read the room and identity out of your existing LiveKit
    access token. Thus your auth/token-minting service does not have to change
    to get audio to flow. Path 1 has no room and no LiveKit token. The worker
    authenticates to the edge with a FirstByte media app key + secret
    (`CLUTCHCALL_MEDIA_KEY` / `CLUTCHCALL_MEDIA_SECRET`). Your **provider**
    keys stay where they are, in your own process.

## Related

  - **[Run a LiveKit Agent on the Transport](/modalities/voice/recipes/livekit-transport)** — Path 1 end to end — install the plugin, register a worker, take a call.
  - **[Keep Your Existing Runtime](/modalities/voice/runtime/keep-existing)** — The transport-only on-ramps in depth — the shim and the raw MoQT primitives.
  - **[LiveKit Agent Integration](/modalities/voice/integrations/livekit)** — Run a LiveKit agent behind our transport, at the integration level.
  - **[Migration Overview](/modalities/voice/migration/overview)** — Choose a path across all the products that you can move off.
  - **[Migration Checklist](/modalities/voice/migration/checklist)** — The wave-by-wave cutover checklist for any voice migration.
