Private channels
Channels whose names start with private- need a token issued by your backend. A token lists the channels it grants: exact names, or prefixes ending in * (private-team-7-*). Your secret never leaves your server: the token is signed with it.
There are two ways to hand out tokens. Use a connection token when you know a user's channels up front, and per-channel authorization when access is decided channel by channel. They combine: thunderclapJS asks for a per-channel token only when the connection token doesn't cover a channel.
Connection tokens
Your backend issues a token for the signed-in user, and thunderclapJS fetches it on every connect and reconnect:
app.get("/api/realtime-token", requireUser, async (req, res) => {
const token = await thunderclap.token({
uid: req.user.id,
channels: [`private-user-${req.user.id}`, "private-team-7-*"],
ttlSeconds: 3600,
});
res.send(token);
});const thunderclap = new Thunderclap({
key: process.env.NEXT_PUBLIC_THUNDERCLAP_KEY,
token: () => fetch("/api/realtime-token").then((r) => r.text()),
});
thunderclap.subscribe("private-user-7");Per-channel authorization
For channels the connection token doesn't cover, thunderclapJS calls authorize, and your backend checks access to that one channel:
const thunderclap = new Thunderclap({
key: process.env.NEXT_PUBLIC_THUNDERCLAP_KEY,
authorize: ({ channel, socketId }) =>
fetch("/api/realtime-authorize", { method: "POST", body: JSON.stringify({ channel, socketId }) })
.then((r) => r.text()),
});app.post("/api/realtime-authorize", requireUser, async (req, res) => {
const { channel } = req.body;
if (!(await canJoin(req.user, channel))) return res.sendStatus(403);
res.send(await thunderclap.token({ uid: req.user.id, channels: [channel], ttlSeconds: 300 }));
});Expiry
Tokens are checked when a client connects or subscribes. An expired token doesn't disconnect the channels it already joined; the client fetches a fresh one on its next reconnect.
A refused subscription emits error on the channel with code 4302. See error codes.