Everything on this page is real pusher-js and Pusher server-library code — the only change from a stock Pusher app is the host. Point the client at the TeleQuick realtime host and your existing code keeps working, because the wire protocol is Pusher Channels either way. Copy a block, drop in your app credentials, and run it.
You need an app’s key, secret, and app ID for these recipes. Mint them in the Realtime Console under Apps & keys — each app is a Pusher-compatible key/secret pair. The secret is shown once; store it as an environment variable, never in client code.
Throughout, the realtime host is realtime.telequick.dev and the control-plane publish API lives on portal.telequick.dev. Replace <APP_KEY>, <APP_ID>, and <APP_SECRET> with your own.

Subscribe to a public channel

Public channels are open — anyone with the app key can subscribe, no auth round trip. The only Pusher option that differs from a hosted Pusher app is wsHost.
import Pusher from "pusher-js";

// Stock pusher-js — the one changed line is wsHost.
const pusher = new Pusher("<APP_KEY>", {
  wsHost: "realtime.telequick.dev",
  wsPort: 443,
  forceTLS: true,
  enabledTransports: ["ws", "wss"],
  cluster: "",           // required by pusher-js's types; ignored when wsHost is set
});

const channel = pusher.subscribe("orders");

channel.bind("order.created", (data) => {
  console.log("new order", data);
});

// Lifecycle you can hook if you want it:
pusher.connection.bind("state_change", ({ current }) => {
  console.log("connection:", current); // connecting → connected → …
});
WebTransport / QUIC transport. The same channels are reachable over WebTransport/QUIC in addition to WebSocket — the wire is the identical Pusher protocol, served on the /app/<APP_KEY> path. Stock pusher-js uses WebSocket; to opt into the QUIC transport, use the TeleQuick realtime client (a drop-in pusher-js-compatible fork that adds WebTransport with automatic WebSocket fallback). The subscribe/bind API is unchanged from the block above. See Realtime — Details for the transport ladder.
wsHost
string
required
The TeleQuick realtime host, realtime.telequick.dev. This is the single line that repoints a stock Pusher client at TeleQuick.
forceTLS
boolean
default:"true"
Always keep TLS on in production — connections are wss:// and QUIC is TLS by construction.
cluster
string
Unused when wsHost is set (there are no Pusher clusters here), but pusher-js still expects the key to be present. Pass an empty string.

Presence channels and a live member list

Presence channels (presence-*) track who is here. They require the same auth step as private channels (below), and in return every subscriber gets the current member roster plus member_added / member_removed events.
1

Subscribe and read the initial roster

pusher:subscription_succeeded fires once with the full member list.
2

Bind to joins and leaves

Keep your local roster in sync as members come and go.
import Pusher from "pusher-js";

const pusher = new Pusher("<APP_KEY>", {
  wsHost: "realtime.telequick.dev",
  wsPort: 443,
  forceTLS: true,
  cluster: "",
  // Auth endpoint your server hosts (see "Private channels" below).
  channelAuthorization: { endpoint: "/pusher/auth" },
});

const presence = pusher.subscribe("presence-room-42");

// Render the whole roster once, on join.
presence.bind("pusher:subscription_succeeded", (members) => {
  const roster = [];
  members.each((m) => roster.push({ id: m.id, ...m.info }));
  console.log("me:", members.me.id, members.me.info);
  render(roster);
});

// Keep it in sync.
presence.bind("pusher:member_added", (member) => {
  console.log("joined:", member.id, member.info);
  render(currentRoster().concat({ id: member.id, ...member.info }));
});

presence.bind("pusher:member_removed", (member) => {
  console.log("left:", member.id);
  render(currentRoster().filter((m) => m.id !== member.id));
});
members.me, member.id, and member.info come straight from the presence_data your auth endpoint returns (next section). The id must be unique per user; info is arbitrary JSON you attach (name, avatar, role).

Private channels and the server auth endpoint

Private (private-*) and presence (presence-*) channels require an HMAC-signed subscription. When a client subscribes, pusher-js POSTs the socket_id and channel_name to your channelAuthorization.endpoint; your server decides whether that user may join and signs the grant with the app secret. The signature is a standard Pusher auth token, so the stock Pusher server library produces it for you.
1

Point the client at your auth endpoint

Set channelAuthorization: { endpoint: "/pusher/auth" } (shown above). On older pusher-js this option is called authEndpoint.
2

Authorize and sign on the server

Run your own access check, then hand the request to the Pusher server library.
import express from "express";
import Pusher from "pusher";

const pusher = new Pusher({
  appId: process.env.APP_ID,
  key: process.env.APP_KEY,
  secret: process.env.APP_SECRET,   // never ships to the browser
  host: "realtime.telequick.dev",
  useTLS: true,
});

const app = express();
app.use(express.urlencoded({ extended: false })); // pusher-js posts form-encoded

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

  // 1. Your own authorization — is req.user allowed on this channel?
  const user = req.user; // from your session/JWT middleware
  if (!user || !mayAccess(user, channel_name)) {
    return res.status(403).send("forbidden");
  }

  // 2. Presence channels also carry the member payload.
  if (channel_name.startsWith("presence-")) {
    const presenceData = {
      user_id: String(user.id),
      user_info: { name: user.name, avatar: user.avatarUrl },
    };
    return res.send(
      pusher.authorizeChannel(socket_id, channel_name, presenceData)
    );
  }

  // 3. Plain private channel.
  res.send(pusher.authorizeChannel(socket_id, channel_name));
});

app.listen(3000);
import os
from flask import Flask, request, jsonify
import pusher

pusher_client = pusher.Pusher(
    app_id=os.environ["APP_ID"],
    key=os.environ["APP_KEY"],
    secret=os.environ["APP_SECRET"],
    host="realtime.telequick.dev",
    ssl=True,
)

app = Flask(__name__)

@app.post("/pusher/auth")
def pusher_auth():
    socket_id = request.form["socket_id"]
    channel = request.form["channel_name"]

    user = current_user()  # from your session
    if user is None or not may_access(user, channel):
        return "forbidden", 403

    if channel.startswith("presence-"):
        payload = pusher_client.authenticate(
            channel=channel,
            socket_id=socket_id,
            custom_data={
                "user_id": str(user.id),
                "user_info": {"name": user.name},
            },
        )
    else:
        payload = pusher_client.authenticate(channel=channel, socket_id=socket_id)

    return jsonify(payload)
The app secret must live only on your server. Anything that can produce these auth tokens can subscribe to any channel, so keep the secret in an environment variable and never inline it in browser code. Rotate it from the console if it leaks.

Trigger an event from your server

Server-side publishing uses the Pusher server library pointed at the TeleQuick host. trigger(channel, event, data) fans the event out across the edge mesh to every subscriber.
import Pusher from "pusher";

const pusher = new Pusher({
  appId: process.env.APP_ID,
  key: process.env.APP_KEY,
  secret: process.env.APP_SECRET,
  host: "realtime.telequick.dev",
  useTLS: true,
});

await pusher.trigger("orders", "order.created", {
  id: 42,
  total: 19.99,
});

// Fan the same event to several channels at once:
await pusher.trigger(["orders", "private-admin"], "order.created", { id: 42 });
pusher_client.trigger("orders", "order.created", {"id": 42, "total": 19.99})
# Publish over the REST API on the control-plane host. Pusher's REST calls are
# signed (auth_key + auth_signature + body_md5); the server libraries above build
# that signature for you, and the console's Event Creator prints a ready-to-run,
# already-signed cURL you can copy.
curl -X POST "https://portal.telequick.dev/apps/<APP_ID>/events" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "order.created",
    "channel": "orders",
    "data": "{\"id\":42,\"total\":19.99}"
  }'
The REST data field is a string — a JSON-encoded string of your payload, not a nested object. The server libraries handle the encoding and the request signature; reach for raw cURL only when you can’t run one, and grab the signed version from the console’s Event Creator.

Send a client event

Subscribers can publish directly to a channel with a client- prefixed event — useful for ephemeral signals like typing indicators or cursor positions, with no server round trip. Client events are only allowed on private-* and presence-* channels, and only when client events are enabled for the app.
const presence = pusher.subscribe("presence-room-42");

presence.bind("pusher:subscription_succeeded", () => {
  // Now safe to emit client events on this channel.
  presence.trigger("client-typing", { userId: "u_42", typing: true });
});

// Everyone else on the channel receives it:
presence.bind("client-typing", (data) => {
  console.log(`${data.userId} is typing`, data.typing);
});
Enable client events for the app first — in the console under Settings → App capabilities. With it off, client-* triggers are silently rejected. Client events are not delivered back to the sender and are rate-limited per connection.

Receive a webhook

TeleQuick POSTs signed event batches to endpoints you register in the console (Webhooks). Each request carries an X-Pusher-Signature header — an HMAC of the raw body under the webhook’s signing secret. Verify it against the raw bytes before parsing, and compare in constant time.
import express from "express";
import crypto from "crypto";

const app = express();

// Capture the RAW body — the signature is over exact bytes, so verify before JSON.parse.
app.post(
  "/hooks/realtime",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.get("X-Pusher-Signature") ?? "";
    const expected = crypto
      .createHmac("sha256", process.env.WEBHOOK_SECRET)
      .update(req.body) // req.body is a Buffer here
      .digest("hex");

    const ok =
      signature.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
    if (!ok) return res.status(401).end();

    const { time_ms, events } = JSON.parse(req.body.toString("utf8"));
    for (const e of events) {
      switch (e.name) {
        case "channel_occupied":
          console.log("first subscriber on", e.channel);
          break;
        case "channel_vacated":
          console.log("last subscriber left", e.channel);
          break;
        case "member_added":
          console.log("member", e.user_id, "joined", e.channel);
          break;
        case "member_removed":
          console.log("member", e.user_id, "left", e.channel);
          break;
      }
    }

    res.status(200).end(); // ack fast; do work off the request path
  }
);

app.listen(3000);
import hmac, hashlib, os, json
from flask import Flask, request

app = Flask(__name__)

@app.post("/hooks/realtime")
def realtime_hook():
    body = request.get_data()  # raw bytes
    signature = request.headers.get("X-Pusher-Signature", "")
    expected = hmac.new(
        os.environ["WEBHOOK_SECRET"].encode(), body, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(signature, expected):
        return "", 401

    payload = json.loads(body)
    for e in payload["events"]:
        print(e["name"], e.get("channel"), e.get("user_id"))
    return "", 200
The signing secret is separate from your app secret and is shown once when you add the webhook. The X-Pusher-Key header also identifies which app key the delivery is for. Channel existence (channel_occupied / channel_vacated) and presence (member_added / member_removed) are shipped. Additional groups shown in the console — client-event and cache-miss webhooks — are rolling out; treat them as preview and confirm delivery for your deployment before you depend on them.

Details

How the Pusher-compatible layer maps onto WebSocket and QUIC transports.

SDK & API

The client, server-publish, and auth surfaces in reference form.

Recipes

End-to-end builds: a live presence list and server-pushed notifications.

Console

Mint app keys, publish test events, and wire webhooks — no code.