Pusher
"Pusher: real-time WebSocket channels, presence channels, private channels, triggers, React hooks"
Pusher provides hosted WebSocket infrastructure for real-time communication between servers and clients. The model is pub/sub: your server triggers events on named channels, and clients subscribe to those channels to receive events instantly. Pusher handles all WebSocket connection management, automatic reconnection, and scaling. Design your real-time features around three channel types — public (no auth), private (server-authenticated), and presence (authenticated with member tracking). Your server is always the source of truth: clients subscribe and react, but state changes flow through your backend, which triggers events after validating and persisting data. Never trigger events directly from the client in production; always route through your server.
## Key Points
- Always use private or presence channels for sensitive data; public channels are readable by anyone with your app key
- Trigger events from the server after persisting data — the server is the authority, Pusher is the notification layer
- Use the `socket_id` exclusion parameter so the sender does not receive their own event (apply optimistic updates instead)
- Batch multi-channel triggers into groups of 10 (Pusher's per-call limit)
- Keep event payloads small (under 10KB); send IDs and let clients fetch full data if needed
- Name channels and events consistently: `private-resource-{id}` for channels, `resource:action` for events
- Monitor connection state via `pusher.connection.bind("state_change", ...)` and show connection status in the UI
- Use presence channels only when you need member lists; they have lower member limits than private channels
- **Triggering events from the client** — Client events bypass your server validation and persistence; always route through your backend
- **Using public channels for user-specific data** — Anyone can subscribe to public channels with just the app key; use private channels for anything non-public
- **Storing state in Pusher** — Pusher is a transport layer, not a database; persist state in your own data store and use Pusher only to notify of changes
- **Large payloads in events** — Pusher has a 10KB payload limit; sending large objects fails silently or gets rejected — send references and let clients fetchskilldb get messaging-services-skills/PusherFull skill: 347 linesPusher Real-Time Channels
Core Philosophy
Pusher provides hosted WebSocket infrastructure for real-time communication between servers and clients. The model is pub/sub: your server triggers events on named channels, and clients subscribe to those channels to receive events instantly. Pusher handles all WebSocket connection management, automatic reconnection, and scaling. Design your real-time features around three channel types — public (no auth), private (server-authenticated), and presence (authenticated with member tracking). Your server is always the source of truth: clients subscribe and react, but state changes flow through your backend, which triggers events after validating and persisting data. Never trigger events directly from the client in production; always route through your server.
Setup
Server-Side Configuration
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!,
useTLS: true,
});
// Basic event trigger
async function triggerEvent(channel: string, event: string, data: unknown) {
await pusher.trigger(channel, event, data);
}
Client-Side React Setup
import PusherJS from "pusher-js";
import { createContext, useContext, useEffect, useRef, useState } from "react";
const pusherClient = new PusherJS(process.env.NEXT_PUBLIC_PUSHER_KEY!, {
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!,
authEndpoint: "/api/pusher/auth",
});
// React context for sharing the Pusher instance
const PusherContext = createContext<PusherJS | null>(null);
function PusherProvider({ children }: { children: React.ReactNode }) {
return (
<PusherContext.Provider value={pusherClient}>
{children}
</PusherContext.Provider>
);
}
function usePusher(): PusherJS {
const pusher = useContext(PusherContext);
if (!pusher) throw new Error("usePusher must be used within PusherProvider");
return pusher;
}
Key Techniques
Channel Subscription Hook
function useChannel(channelName: string) {
const pusher = usePusher();
const [channel, setChannel] = useState<PusherJS.Channel | null>(null);
useEffect(() => {
const ch = pusher.subscribe(channelName);
setChannel(ch);
ch.bind("pusher:subscription_succeeded", () => {
console.log(`Subscribed to ${channelName}`);
});
ch.bind("pusher:subscription_error", (error: unknown) => {
console.error(`Subscription error on ${channelName}:`, error);
});
return () => {
pusher.unsubscribe(channelName);
};
}, [pusher, channelName]);
return channel;
}
function useEvent<T = unknown>(
channel: PusherJS.Channel | null,
eventName: string,
callback: (data: T) => void
) {
const callbackRef = useRef(callback);
callbackRef.current = callback;
useEffect(() => {
if (!channel) return;
const handler = (data: T) => callbackRef.current(data);
channel.bind(eventName, handler);
return () => {
channel.unbind(eventName, handler);
};
}, [channel, eventName]);
}
Private Channels with Server Authentication
// Server-side auth endpoint
import express from "express";
const app = express();
app.use(express.json());
app.post("/api/pusher/auth", (req, res) => {
const { socket_id, channel_name } = req.body;
const userId = req.session?.userId; // your auth middleware
if (!userId) {
res.status(403).json({ error: "Unauthorized" });
return;
}
// Validate user has access to this channel
if (!userCanAccessChannel(userId, channel_name)) {
res.status(403).json({ error: "Forbidden" });
return;
}
const authResponse = pusher.authorizeChannel(socket_id, channel_name);
res.json(authResponse);
});
function userCanAccessChannel(userId: string, channelName: string): boolean {
// Private channels start with "private-"
if (channelName.startsWith("private-user-")) {
const channelUserId = channelName.replace("private-user-", "");
return channelUserId === userId;
}
return true;
}
// Client usage
function UserNotifications({ userId }: { userId: string }) {
const channel = useChannel(`private-user-${userId}`);
useEvent<{ title: string; body: string }>(channel, "notification", (data) => {
showToast(data.title, data.body);
});
return null;
}
Presence Channels for Online Status
// Server-side presence auth
app.post("/api/pusher/auth", (req, res) => {
const { socket_id, channel_name } = req.body;
const user = getCurrentUser(req);
if (channel_name.startsWith("presence-")) {
const presenceData = {
user_id: user.id,
user_info: {
name: user.name,
avatar: user.avatarUrl,
},
};
const auth = pusher.authorizeChannel(socket_id, channel_name, presenceData);
res.json(auth);
return;
}
const auth = pusher.authorizeChannel(socket_id, channel_name);
res.json(auth);
});
// React hook for presence
interface PresenceMember {
id: string;
info: { name: string; avatar: string };
}
function usePresenceChannel(channelName: string) {
const pusher = usePusher();
const [members, setMembers] = useState<Map<string, PresenceMember>>(new Map());
const [channel, setChannel] = useState<PusherJS.PresenceChannel | null>(null);
useEffect(() => {
const ch = pusher.subscribe(channelName) as PusherJS.PresenceChannel;
setChannel(ch);
ch.bind("pusher:subscription_succeeded", (memberData: { members: Record<string, unknown> }) => {
const memberMap = new Map<string, PresenceMember>();
Object.entries(memberData.members).forEach(([id, info]) => {
memberMap.set(id, { id, info: info as { name: string; avatar: string } });
});
setMembers(memberMap);
});
ch.bind("pusher:member_added", (member: { id: string; info: { name: string; avatar: string } }) => {
setMembers((prev) => new Map(prev).set(member.id, member));
});
ch.bind("pusher:member_removed", (member: { id: string }) => {
setMembers((prev) => {
const next = new Map(prev);
next.delete(member.id);
return next;
});
});
return () => {
pusher.unsubscribe(channelName);
};
}, [pusher, channelName]);
return { channel, members };
}
Server-Side Event Triggers
// Trigger to a single channel
async function notifyUser(userId: string, event: string, data: unknown) {
await pusher.trigger(`private-user-${userId}`, event, data);
}
// Trigger to multiple channels (max 10 per call)
async function broadcastToRooms(roomIds: string[], event: string, data: unknown) {
const channels = roomIds.map((id) => `private-room-${id}`);
// Batch into groups of 10
for (let i = 0; i < channels.length; i += 10) {
const batch = channels.slice(i, i + 10);
await pusher.trigger(batch, event, data);
}
}
// Exclude a specific socket from receiving the event
async function triggerExcludingSender(
channel: string,
event: string,
data: unknown,
socketId: string
) {
await pusher.trigger(channel, event, data, { socket_id: socketId });
}
Real-Time Chat Example
// Server endpoint for sending messages
app.post("/api/messages", async (req, res) => {
const { channelId, text } = req.body;
const user = getCurrentUser(req);
// Persist the message
const message = await db.messages.create({
channelId,
userId: user.id,
text,
createdAt: new Date(),
});
// Broadcast to channel, exclude sender's socket
await pusher.trigger(
`private-room-${channelId}`,
"message:new",
{
id: message.id,
text: message.text,
user: { id: user.id, name: user.name },
createdAt: message.createdAt,
},
{ socket_id: req.body.socketId }
);
res.json(message);
});
// React chat component
function ChatRoom({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<Message[]>([]);
const channel = useChannel(`private-room-${roomId}`);
useEvent<Message>(channel, "message:new", (message) => {
setMessages((prev) => [...prev, message]);
});
useEvent(channel, "message:deleted", (data: { id: string }) => {
setMessages((prev) => prev.filter((m) => m.id !== data.id));
});
const sendMessage = async (text: string) => {
const socketId = pusherClient.connection.socket_id;
const response = await fetch("/api/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ channelId: roomId, text, socketId }),
});
const message = await response.json();
setMessages((prev) => [...prev, message]); // Optimistic add
};
return (
<div>
{messages.map((m) => (
<div key={m.id}>{m.user.name}: {m.text}</div>
))}
<MessageInput onSend={sendMessage} />
</div>
);
}
Best Practices
- Always use private or presence channels for sensitive data; public channels are readable by anyone with your app key
- Trigger events from the server after persisting data — the server is the authority, Pusher is the notification layer
- Use the
socket_idexclusion parameter so the sender does not receive their own event (apply optimistic updates instead) - Batch multi-channel triggers into groups of 10 (Pusher's per-call limit)
- Keep event payloads small (under 10KB); send IDs and let clients fetch full data if needed
- Name channels and events consistently:
private-resource-{id}for channels,resource:actionfor events - Monitor connection state via
pusher.connection.bind("state_change", ...)and show connection status in the UI - Use presence channels only when you need member lists; they have lower member limits than private channels
Anti-Patterns
- Triggering events from the client — Client events bypass your server validation and persistence; always route through your backend
- Using public channels for user-specific data — Anyone can subscribe to public channels with just the app key; use private channels for anything non-public
- Storing state in Pusher — Pusher is a transport layer, not a database; persist state in your own data store and use Pusher only to notify of changes
- Large payloads in events — Pusher has a 10KB payload limit; sending large objects fails silently or gets rejected — send references and let clients fetch
- Subscribing to too many channels — Each client has a connection limit (typically 100 channels); aggregate related events into fewer channels
- Not handling disconnection — Users lose connection on mobile or spotty networks; always handle reconnection and fetch missed state from your API
- Creating channels without cleanup — Unused channels consume resources; design your channel naming so they naturally become inactive when users leave
Install this skill directly: skilldb add messaging-services-skills
Related Skills
Ably
"Ably: real-time messaging, pub/sub, presence, message history, push notifications, WebSocket/SSE"
Knock
"Knock: notification infrastructure, in-app feeds, workflows, preferences, channels, React components"
Novu
"Novu: notification infrastructure, multi-channel (email/SMS/push/in-app), templates, preferences, digest, workflows"
OneSignal
"OneSignal: push notifications, in-app messaging, email, SMS, segments, journeys, REST API"
Sendbird
"Sendbird: chat SDK, group channels, open channels, messages, file sharing, moderation, UIKit components"
Stream Chat
"Stream (GetStream): chat SDK, channels, threads, reactions, typing indicators, moderation, React components"