# Recipes

> End-to-end realtime builds on FirstByte: a live presence list and server-pushed notifications, with stock pusher-js and the Pusher server libraries.

This page has two complete, runnable builds. Each combines a channel type, an
auth endpoint, and a server publisher into a real feature. Both use **stock
`pusher-js`** and the **stock Pusher server libraries**. Only the host changes.
Everything below drops straight into an existing Pusher codebase.

  - **[Live presence list](#recipe-1-live-presence-list)** — Build a "who's online" roster on a `presence-*` channel. An auth endpoint
    signs the subscription. A client tracks members that join and leave in real
    time.
  - **[Server-pushed notifications](#recipe-2-server-pushed-notifications)** — Your backend triggers events. The browser binds and renders them. The
    feature continues to work across reconnects because channel state is
    restored on resubscribe.

> **NOTE:**
> Every snippet assumes that you created an app in the
> [Realtime Console](/modalities/realtime/console). You need its **key**,
> **secret**, and **app ID**. The key is public (it ships in the browser bundle).
> The secret is server-only.

## The one-line switch

To run on FirstByte, change only the host. Keep your key, your channel
names, your event names, and your handlers.

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

// Point stock pusher-js at FirstByte — nothing else changes.
const pusher = new Pusher("<APP_KEY>", {
  wsHost:   "realtime.firstbyte.fast",
  forceTLS: true,
  // Private/presence channels call this endpoint to sign the subscription.
  channelAuthorization: { endpoint: "/api/realtime/auth" },
});
```

**Pusher server library (Node):**
```ts
import Pusher from "pusher";

// Same server library you already use — just add `host`.
export const realtime = new Pusher({
  appId:  process.env.REALTIME_APP_ID!,
  key:    process.env.REALTIME_APP_KEY!,
  secret: process.env.REALTIME_APP_SECRET!,
  host:   "realtime.firstbyte.fast",
  useTLS: true,
});
```

**Pusher server library (Python):**
```python
import os
from pusher import Pusher

realtime = Pusher(
    app_id=os.environ["REALTIME_APP_ID"],
    key=os.environ["REALTIME_APP_KEY"],
    secret=os.environ["REALTIME_APP_SECRET"],
    host="realtime.firstbyte.fast",
    ssl=True,
)
```

> **TIP:**
> To use the QUIC transport instead of WebSocket, swap `pusher-js` for the
> WebTransport/QUIC-capable fork. The fork speaks the **identical** Pusher
> protocol over an HTTP/3 connection (`/app/<key>`). The recipes below are
> unchanged. See [SDK & API](/modalities/realtime/sdk-methods) for which client
> to install.

## Recipe 1 — Live presence list

**Presence channels** (`presence-*`) are private channels that also carry a
member roster. On subscribe, you get the current members and every subsequent
join and leave. You can render "who's online" with no polling.

Presence channels are authorized, so you need a small **auth endpoint**.
FirstByte checks its HMAC exactly like Pusher does. Sign the subscription
with the stock server library's `authorizeChannel`.

  1. **Add the auth endpoint**
When the browser subscribes to a `private-*` or `presence-*` channel,
    `pusher-js` POSTs the `socket_id` and `channel_name` to your
    `channelAuthorization.endpoint`. Return the signed payload. For presence,
    attach the member's `user_id` and public `user_info`. Other clients see
    these fields in the roster. Put only display-safe fields here.

    
    **Node (Express):**
```ts
    import express from "express";
    import { realtime } from "./realtime"; // the server client from above

    const app = express();
    app.use(express.urlencoded({ extended: false }));

    app.post("/api/realtime/auth", (req, res) => {
      const { socket_id, channel_name } = req.body;

      // 🔒 Authenticate the *real* user from your own session/cookie first.
      const user = req.session?.user;
      if (!user) return res.status(403).send("not signed in");

      // Optionally scope who may join which room.
      if (channel_name.startsWith("presence-") && !user.canJoin(channel_name)) {
        return res.status(403).send("forbidden");
      }

      const authResponse = realtime.authorizeChannel(socket_id, channel_name, {
        user_id:   user.id,                         // required, unique per member
        user_info: { name: user.name, avatar: user.avatar },
      });
      res.send(authResponse);
    });
    ```

    **Python (Flask):**
```python
    from flask import Flask, request, session, jsonify, abort
    from realtime_client import realtime  # the server client from above

    app = Flask(__name__)

    @app.post("/api/realtime/auth")
    def realtime_auth():
        socket_id    = request.form["socket_id"]
        channel_name = request.form["channel_name"]

        user = session.get("user")
        if not user:
            abort(403)

        if channel_name.startswith("presence-") and not can_join(user, channel_name):
            abort(403)

        auth = realtime.authenticate(
            channel=channel_name,
            socket_id=socket_id,
            custom_data={
                "user_id": user["id"],
                "user_info": {"name": user["name"], "avatar": user["avatar"]},
            },
        )
        return jsonify(auth)
    ```
    

    > **WARNING:**
> The auth endpoint is your trust boundary. Never take the `user_id` from the
>     browser. Derive it from your own authenticated session. A caller of this
>     endpoint can appear in the roster as any identity that the endpoint
>     returns.

  2. **Subscribe and render the roster**
On the client, subscribe to a `presence-*` channel and read `members`. The
    `pusher:subscription_succeeded` event gives you the initial roster.
    `pusher:member_added` / `pusher:member_removed` keep the roster live.

    ```js
    import Pusher from "pusher-js";

    const pusher = new Pusher("<APP_KEY>", {
      wsHost:   "realtime.firstbyte.fast",
      forceTLS: true,
      channelAuthorization: { endpoint: "/api/realtime/auth" },
    });

    const roster = new Map(); // user_id -> user_info
    const channel = pusher.subscribe("presence-lobby");

    // 1. Initial roster on join.
    channel.bind("pusher:subscription_succeeded", (members) => {
      roster.clear();
      members.each((m) => roster.set(m.id, m.info));
      console.log("me:", members.me.id, "· online:", members.count);
      render();
    });

    // 2. Live joins and leaves.
    channel.bind("pusher:member_added",   (m) => { roster.set(m.id, m.info); render(); });
    channel.bind("pusher:member_removed", (m) => { roster.delete(m.id);     render(); });

    // 3. Auth failures surface here (bad session, 403 from your endpoint).
    channel.bind("pusher:subscription_error", (err) =>
      console.error("presence auth failed:", err.status));

    function render() {
      const names = [...roster.values()].map((u) => u.name);
      document.querySelector("#online").textContent =
        `${names.length} online: ${names.join(", ")}`;
    }
    ```

  3. **React to presence from your backend (optional)**
Register the **Presence** webhook group in the console to receive
    `member_added` / `member_removed` server-side. This is useful for "mark
    user offline" logic without an open socket. See
    [console → webhooks](/modalities/realtime/console#wire-and-manage-webhooks)
    for the delivery format and for how to check the `X-Pusher-Signature` HMAC.

> **NOTE:**
> A member's presence is derived from their live connection. FirstByte sends
> `member_removed` when a client's last tab closes, or when the network drops for
> long enough. The roster corrects itself without a client-side heartbeat.
> Fan-out spans shards and edge nodes automatically. You never run or name a
> message broker.

## Recipe 2 — Server-pushed notifications

This is the classic pattern. Your backend does work (an order ships, a job
finishes). You want the result to appear instantly in the user's browser.
Publish from the server with `trigger()`. Bind on the client.

Use a **private per-user channel** (`private-notifications-<userId>`). Then
only the authorized user receives their notifications. Reuse the same auth
endpoint from Recipe 1.

  1. **Publish from the server**
Call `trigger(channel, event, data)` anywhere in your backend. That single
    call fans the event out to every connected subscriber of that channel
    across the mesh.

    
    **Node:**
```ts
    import { realtime } from "./realtime";

    export async function notify(userId: string, note: unknown) {
      await realtime.trigger(`private-notifications-${userId}`, "notification", note);
    }

    // e.g. from an order-shipped handler:
    await notify("u_42", {
      id:    "n_8f1",
      kind:  "order_shipped",
      title: "Your order is on its way",
      at:    new Date().toISOString(),
    });
    ```

    **Python:**
```python
    from datetime import datetime, timezone
    from realtime_client import realtime

    def notify(user_id: str, note: dict) -> None:
        realtime.trigger(f"private-notifications-{user_id}", "notification", note)

    notify("u_42", {
        "id": "n_8f1",
        "kind": "order_shipped",
        "title": "Your order is on its way",
        "at": datetime.now(timezone.utc).isoformat(),
    })
    ```

    **REST publish:**
```bash
    # The control-plane REST publish API mirrors trigger() for scripts and
    # services that don't run a Pusher server library.
    curl -X POST "https://portal.firstbyte.fast/apps/$REALTIME_APP_ID/events" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "notification",
        "channels": ["private-notifications-u_42"],
        "data": "{\"title\":\"Your order is on its way\"}"
      }'
    ```
    

    > **NOTE:**
> The REST body's `data` is a **string**, exactly as in the Pusher HTTP API.
>     JSON-encode your payload before you put it in the field. The server
>     libraries do this for you. The server libraries also do the authenticated
>     publish (they sign the request with your app secret). See
>     [console → publish a test event](/modalities/realtime/console#publish-a-test-event)
>     to try it from the browser first.

  2. **Bind on the client**
Subscribe to the user's private channel and bind the event. To show a
    toast, increment a badge, or play a sound, write a handler.

    ```js
    import Pusher from "pusher-js";

    const pusher = new Pusher("<APP_KEY>", {
      wsHost:   "realtime.firstbyte.fast",
      forceTLS: true,
      channelAuthorization: { endpoint: "/api/realtime/auth" },
    });

    const channel = pusher.subscribe(`private-notifications-${CURRENT_USER_ID}`);

    channel.bind("notification", (note) => {
      showToast(note.title);
      bumpUnreadBadge();
    });
    ```

  3. **Survive reconnects**
Connections drop. Laptops sleep. Phones switch from WiFi to cellular. You
    do **not** re-subscribe by hand. When the socket comes back, `pusher-js`
    replays your subscriptions and runs your auth endpoint again automatically.
    Where possible, FirstByte carries the connection across a network change
    without a full reconnect. Bind the connection lifecycle to drive UI.
    **Backfill** any events that were possibly published while you were
    offline. The channel is live-only, so durable state comes from your own
    API.

    ```js
    // Reflect transport health in the UI.
    pusher.connection.bind("state_change", ({ current }) => {
      setBanner(current === "connected" ? null : `reconnecting… (${current})`);
    });

    // On (re)connect, reconcile with the source of truth.
    pusher.connection.bind("connected", async () => {
      const missed = await fetch("/api/notifications?since=" + lastSeenId).then((r) => r.json());
      for (const note of missed) applyNotification(note);
    });

    // Optional: react to a hard failure (e.g. app disabled, key revoked).
    pusher.connection.bind("error", (err) => console.error("realtime error:", err));
    ```

    > **TIP:**
> Treat realtime as the **fast path**, not the system of record. On
>     reconnect, a short `GET …?since=<cursor>` closes the gap for the rare
>     window where the socket was down while your server published. The pattern
>     is the same one that you use with hosted Pusher.

> **NOTE:**
> Notification fan-out uses the same `trigger()` → subscriber path as presence.
> The path spans shards and edge nodes. You publish once. The mesh delivers to
> every online subscriber of the channel.

## Honest gaps

> **NOTE:**
> **Shipped and used above:** public / `private-*` / `presence-*` channels, HMAC
> subscription auth, `client-*` events, server `trigger()`, the REST publish API,
> and the channel-existence + presence webhook groups.
> 
> **Still rolling out:** some webhook and batch-delivery features (for example,
> batched event webhooks and larger multi-channel publishes) are in **preview**.
> They can lag the hosted Pusher feature set. If a build depends on one of them,
> check it in the [Realtime Console](/modalities/realtime/console) for your app
> before you ship.

## Related

  - **[Details](/modalities/realtime/details)** — The channel model, transports (WebSocket and QUIC), and how fan-out works.
  - **[SDK & API](/modalities/realtime/sdk-methods)** — The client, server-publish, and auth surfaces, and which pusher-js to use.
  - **[Cookbook](/modalities/realtime/cookbook)** — Shorter copy-paste snippets: subscribe, private auth, client events, webhooks.
  - **[Console](/modalities/realtime/console)** — Create apps and keys, publish test events, see live stats, and configure
    webhooks.
