Overview
Pusher Channels is publish/subscribe over WebSockets. The server SDK (pusher, 5.x) publishes events through Pusher's HTTP API; the browser library (pusher-js, 8.x) subscribes to channels and falls back to HTTP streaming or polling on restrictive networks. Channel type is decided by the name prefix: no prefix is public, private- needs server authorization, presence- needs authorization plus user identity, private-encrypted- adds end-to-end encryption. The app id, key, secret and cluster come from the Channels app in the Pusher dashboard; the secret never leaves the server.
Instructions
- Install:
npm install pusheron the server,npm install pusher-jsin the client. - Create one server instance.
triggerandtriggerBatchreturn promises, soawaitthem and catchPusher.RequestError. - Trigger limits: event data at most 10 KB, one
triggerto at most 100 channels,triggerBatchat most 10 events, channel names at most 164 characters including theprivate-/presence-prefix (letters, digits and_ - = @ , . ;). - Authorize private and presence channels with
pusher.authorizeChannel(socketId, channelName, presenceData?). pusher-js POSTssocket_idandchannel_nameas form-encoded fields tochannelAuthorization.endpoint(default/pusher/auth), so the server needsexpress.urlencoded(). Always check that the logged-in user may join that channel before signing. - In the client set
channelAuthorization: { endpoint: "/api/pusher/auth" }. The oldauthEndpointoption is the pre-v7 name; use the new one. - Client events (typing, cursors) must be enabled in the app settings, only work on private and presence channels, and event names must start with
client-. - To exclude the sender from receiving its own server event, pass
{ socket_id }as the fourth argument oftrigger. - Verify webhooks (channel_occupied, channel_vacated, member_added, member_removed) with
pusher.webhook({ headers, rawBody }).isValid(); use the raw request body, not parsed JSON.
Examples
Server: trigger, batch and channel auth (Express)
import express from "express";
import Pusher from "pusher";
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID!,
key: process.env.PUSHER_KEY!,
secret: process.env.PUSHER_SECRET!,
cluster: process.env.PUSHER_CLUSTER ?? "eu",
useTLS: true,
});
const app = express();
app.use(express.urlencoded({ extended: false }));
// "Notify the warehouse dashboard about order 8841"
await pusher.trigger("orders", "new-order", { id: "ord-8841", total: 99.99 });
await pusher.triggerBatch([
{ channel: "private-user-42", name: "notification", data: { message: "Invoice paid" } },
{ channel: "dashboard", name: "metric-update", data: { activeUsers: 1234 } },
]);
app.post("/api/pusher/auth", (req, res) => {
const { socket_id, channel_name } = req.body;
const user = req.user; // set by your session middleware
if (!user || !userCanAccessChannel(user, channel_name)) {
return res.status(403).json({ error: "Forbidden" });
}
if (channel_name.startsWith("presence-")) {
return res.json(
pusher.authorizeChannel(socket_id, channel_name, {
user_id: String(user.id),
user_info: { name: user.name },
})
);
}
res.json(pusher.authorizeChannel(socket_id, channel_name));
});
Client: live metric and presence list (React)
import Pusher from "pusher-js";
import { useEffect, useState } from "react";
const pusher = new Pusher(process.env.NEXT_PUBLIC_PUSHER_KEY!, {
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!,
channelAuthorization: { endpoint: "/api/pusher/auth", transport: "ajax" },
});
export function ActiveUsers() {
const [count, setCount] = useState<number | null>(null);
useEffect(() => {
const channel = pusher.subscribe("dashboard");
const onMetric = (p: { activeUsers: number }) => setCount(p.activeUsers);
channel.bind("metric-update", onMetric);
return () => {
channel.unbind("metric-update", onMetric);
pusher.unsubscribe("dashboard");
};
}, []);
return <div>Active users: {count ?? "..."}</div>;
}
type Member = { id: string; info: { name: string } };
export function OnlineUsers({ roomId }: { roomId: string }) {
const [members, setMembers] = useState<Member[]>([]);
useEffect(() => {
const name = `presence-room-${roomId}`;
const channel = pusher.subscribe(name);
channel.bind("pusher:subscription_succeeded", (m: any) => {
const list: Member[] = [];
m.each((member: Member) => list.push(member));
setMembers(list);
});
channel.bind("pusher:member_added", (m: Member) => setMembers((prev) => [...prev, m]));
channel.bind("pusher:member_removed", (m: Member) =>
setMembers((prev) => prev.filter((p) => p.id !== m.id))
);
return () => pusher.unsubscribe(name);
}, [roomId]);
return <ul>{members.map((m) => <li key={m.id}>{m.info.name}</li>)}</ul>;
}
Result: the dashboard number updates the moment the server calls trigger, and the list gains or loses a name as users join or leave the room.
Webhook endpoint
app.post("/webhooks/pusher", express.raw({ type: "application/json" }), (req, res) => {
const hook = pusher.webhook({ headers: req.headers as Record<string, string>, rawBody: req.body.toString("utf8") });
if (!hook.isValid()) return res.sendStatus(401);
for (const ev of hook.getEvents()) console.log(ev.name, ev.channel);
res.sendStatus(200);
});
Guidelines
- Keep payloads small: send an id and let the client fetch details. Over 10 KB the API rejects the trigger.
- Never expose
PUSHER_SECRETto the browser; only the key and cluster are public. Do not mark the secretNEXT_PUBLIC_. - A public channel is readable by anyone who knows its name. Put user-specific data on
private-channels. - Presence channels are limited to 100 members, 1 KB of
user_infoper member and 128-character user ids; use them for rosters, not large audiences. - Mount the webhook route with a raw-body parser; a JSON parser changes the bytes and breaks the signature.
- Pusher delivers events live only: a client that was offline misses them. Store messages in your database and load history on connect.
- Always unbind handlers and unsubscribe on cleanup, or React strict mode will deliver duplicates.