# Details

> A drop-in Pusher Channels alternative. Migrate a stock pusher-js app when you change one URL. Public, private, and presence channels run over WebSocket and QUIC.

Realtime is a **Pusher Channels-compatible** publish/subscribe layer for browser
and mobile clients. It speaks the Pusher protocol on the wire. The tools you
already use work against FirstByte unchanged: stock `pusher-js` in the
browser and the official Pusher server libraries on the backend. To migrate an
existing Pusher app, point the client at the FirstByte host. You change
**one URL**. You do not swap the SDK, rewrite the protocol, or learn a new
channel model.

Use it for live dashboards, presence ("who's here"), in-app notifications,
activity feeds, and collaborative UIs. It fits any case where a browser needs a
durable, low-latency stream of events. You do not operate a socket cluster.

## The one-URL migration

If you already have a Pusher app, the whole migration is the connection config.
Keep your channel names, your client event handlers, your auth endpoint, and your
server-side `trigger()` calls. Change where the client connects.

**Before (Pusher):**
```js
import Pusher from "pusher-js";

const pusher = new Pusher("<your-key>", {
  cluster: "us2",
});
```

**After (FirstByte):**
```js
import Pusher from "pusher-js";

const pusher = new Pusher("<your-key>", {
  wsHost: "realtime.firstbyte.fast",
  forceTLS: true,
  enabledTransports: ["ws", "wss"],
});
```

Everything downstream is identical:

```js
const channel = pusher.subscribe("presence-orders");
channel.bind("order.created", (data) => render(data));
```

> **NOTE:**
> You create app credentials (`app_id`, `key`, and `secret`) in the
> [Realtime Console](/modalities/realtime/console). Create a key there. Put it in
> the config above. Then you are connected.

## Two clients: pusher-js or the QUIC-native SDK

You connect with one of **two** first-class clients. Both speak the same Pusher
protocol. Both use the same channels/events/presence API, the same auth
endpoint, and the same server-side publish. The choice is only client-side. You
can run different clients per platform against one app.

  - **Stock pusher-js (WebSocket)** — This is unmodified `pusher-js` over `wss://realtime.firstbyte.fast/app/<key>`.
    It is the zero-change path. You add no dependency and swap no import. You
    only repoint `wsHost`. Every official Pusher SDK works without changes. Use
    it for a pure drop-in migration on normal, wired networks.
  - **FirstByte QUIC-native client** — This is a drop-in, **pusher-js-API-compatible** client from the
    FirstByte SDK. It has the same constructor and the same
    channels/events/presence API. It prefers WebTransport/QUIC and
    automatically falls back down a transport ladder. Use it when clients run
    on lossy, mobile, or high-latency networks.

### Comparison

| Client | Transport | When to use |
| --- | --- | --- |
| Stock `pusher-js` | WebSocket (`ws` / `wss`) | Pure drop-in migration; no new dependency; normal/wired networks where WebSocket-over-TCP is already fine. |
| FirstByte QUIC-native client | Ladder: `webtransport` → `ws` → `sockjs` | Mobile / lossy / high-latency clients that need faster reconnects and no TCP head-of-line blocking. |
| FirstByte native mobile SDK | Raw QUIC | Native iOS/Android apps that want QUIC end-to-end without a browser WebTransport hop. |

### Using the QUIC-native client

The QUIC-native client is a separate first-party package. It is a drop-in
replacement for the `pusher-js` import. It ships in the FirstByte SDK under
the `realtime` subpath and exposes a **Pusher-compatible constructor**. Only
the import line changes.

**Stock pusher-js (WebSocket):**
```js
import Pusher from "pusher-js";

const pusher = new Pusher("<your-key>", {
  wsHost: "realtime.firstbyte.fast",
  forceTLS: true,
  enabledTransports: ["ws", "wss"],
});
```

**QUIC-native (drop-in):**
```js
// Same Pusher-compatible API — only the import changes.
import Pusher from "@clutchcall/agents/realtime";

const pusher = new Pusher("<your-key>", {
  wsHost: "realtime.firstbyte.fast",
  forceTLS: true,
});

// Everything downstream is identical to stock pusher-js:
const channel = pusher.subscribe("presence-orders");
channel.bind("order.created", (data) => render(data));
```

In the browser, the QUIC path reaches the engine over **WebTransport (HTTP/3)**.
It uses the same `/app/<key>` handler as WebSocket. If the client cannot
establish WebTransport, it steps down to `ws`, and then to `sockjs`. A
connection is always made. You do not write fallback logic.

> **NOTE:**
> **When the QUIC client helps.** On a clean, wired network, WebSocket-over-TCP
> is already fast and reliable. The QUIC-native client does not make it
> meaningfully faster. The QUIC-native client wins on **lossy, mobile, and
> high-latency** networks. QUIC avoids TCP head-of-line blocking: one lost packet
> does not stall every channel. QUIC also reconnects faster via 0-RTT session
> resumption. Use stock `pusher-js` for a straight drop-in migration. Then move
> the mobile and flaky-network clients to the QUIC-native client.

> **NOTE:**
> The browser WebTransport server plane is live. **Raw QUIC end-to-end** (no
> WebTransport hop) is the **native mobile SDK** path. Adopt it through the
> FirstByte native iOS/Android SDK, not a browser bundle. If you are not sure
> that the native raw-QUIC path fits your app, start on the QUIC-native
> browser/JS client. The channel code is identical for both paths.

## The channel model

Realtime has the same three channel types as Pusher, the same naming
conventions, and the same authorization rules. The channel prefix selects the
behavior.

  - **Public** — Any client may subscribe. There is no authorization. Use it for open
    broadcast: public status, live scores, and data that is not per-user.
  - **Private** — These channels are named `private-*`. **Your** auth endpoint HMAC-signs
    each subscription, so only authorized clients can join. Use them for
    per-user or per-tenant streams.
  - **Presence** — These channels are named `presence-*`. A presence channel is a private
    channel that also tracks membership. Each subscriber contributes member
    info. Every subscriber gets a live who-is-here roster with add/remove
    events.

### Authorizing private and presence channels

Private and presence subscriptions are authorized exactly as with Pusher. The
client asks **your** server to sign the subscription. FirstByte validates
the signed token against your app secret. Point `pusher-js` at your existing
auth route. No other change is necessary.

```js
const pusher = new Pusher("<your-key>", {
  wsHost: "realtime.firstbyte.fast",
  forceTLS: true,
  channelAuthorization: {
    endpoint: "https://your-app.example.com/pusher/auth",
  },
});
```

Your auth endpoint continues to use the Pusher server library's
`authorizeChannel()` (or `authenticate()` on older versions). The signing logic
does not change.

### Client events

On private and presence channels, clients can send **client events** (names
prefixed `client-*`). These events fan out to the other subscribers without a
server round-trip. Use them for ephemeral, high-frequency signals that do not
need to touch your backend: typing indicators, cursor positions, live drawing.
Client events are not allowed on public channels. This matches Pusher.

```js
channel.trigger("client-cursor-move", { x, y });
```

## The transport ladder

The wire semantics are identical for both clients: channels, events, auth, and
client events. Both use the same `/app/<key>` handler on FirstByte. Only the
transport differs:

  1. **WebTransport / QUIC**
This is the QUIC-native client's preferred rung. In the browser this is
    WebTransport over HTTP/3. Native mobile SDKs can use raw QUIC. This rung
    helps most on lossy and mobile networks.
  2. **WebSocket**
Stock `pusher-js` uses this rung directly. The QUIC-native client falls
    back to it when it cannot establish WebTransport. The address is
    `wss://realtime.firstbyte.fast/app/<key>`.
  3. **SockJS**
This is the final fallback rung. It serves restrictive networks that block
    both of the rungs above. A connection is always made.

> **NOTE:**
> Start on WebSocket for a drop-in migration. Move latency- or mobility-sensitive
> clients to the QUIC-native client later. Your channel code, auth, and server
> publish do not change.

## Publishing from your server

For server-side publishing, use the Pusher server library pointed at
FirstByte. Configure it with your app credentials and host. Then call
`trigger()`.

  
    ```js
    import Pusher from "pusher";

    const pusher = new Pusher({
      appId: "<your-app-id>",
      key: "<your-key>",
      secret: "<your-secret>",
      host: "realtime.firstbyte.fast",
      useTLS: true,
    });

    await pusher.trigger("presence-orders", "order.created", {
      id: "ord_123",
      total: 4200,
    });
    ```
  
  
    ```python
    import pusher

    client = pusher.Pusher(
        app_id="<your-app-id>",
        key="<your-key>",
        secret="<your-secret>",
        host="realtime.firstbyte.fast",
        ssl=True,
    )

    client.trigger("presence-orders", "order.created", {
        "id": "ord_123",
        "total": 4200,
    })
    ```
  
  
    ```bash
    # Publish over the control-plane REST API
    curl -X POST "https://portal.firstbyte.fast/apps/<app-id>/events" \
      -H "Content-Type: application/json" \
      -d '{"name":"order.created","channel":"presence-orders","data":"{\"id\":\"ord_123\"}"}'
    ```
  

See [SDK & API](/modalities/realtime/sdk-methods) for the full client and server
surface, and the [Cookbook](/modalities/realtime/cookbook) for end-to-end
examples.

## Webhooks

Realtime can notify your backend about the channel lifecycle over signed
webhooks. The events are **channel occupied** and **channel vacated**, plus
**member added** and **member removed** on presence channels. Configure
webhooks in the [Realtime Console](/modalities/realtime/console).

> **WARNING:**
> Channel-occupancy and presence-member webhooks are the shipped set. Some
> higher-level webhook features (such as batched delivery) are still rolling out.
> Treat those features as preview. Check their availability in the console before
> you depend on them.

## Under the hood

Your events fan out across many shards and nodes in an edge mesh. A channel's
subscribers can be spread across the fleet and still see every event in order.
You do not operate any of this. You run no socket cluster, no presence store,
and no fan-out tier. The other modalities share the same operational model:
connect a client, publish, and the mesh delivers.

> **NOTE:**
> **Shipped today:** public channels, `private-*` with HMAC subscription auth,
> `presence-*` with membership, `client-*` events, and REST publish. These are
> the core features and are production-ready. Newer webhook/batch features are
> noted as preview above.

## Related

  - **[SDK & API](/modalities/realtime/sdk-methods)** — The full `pusher-js` client, server publish, and auth surfaces against
    FirstByte.
  - **[Console](/modalities/realtime/console)** — Create app keys, see live channel stats, and configure webhooks. No code is
    necessary.
  - **[Cookbook](/modalities/realtime/cookbook)** — End-to-end, runnable examples: presence rosters, notifications, dashboards.
  - **[Recipes](/modalities/realtime/recipes)** — Focused patterns for common realtime UI problems.
