Migrating from Pusher
Thunderclap understands Pusher's protocol and REST API, so a migration takes two steps. First, point your existing Pusher libraries at Thunderclap by changing the host: nothing else changes, and your authorization endpoint keeps working as it is. Then move to thunderclapJS and @thunderclapjs/server at your own pace, page by page if you like: clients on both libraries share the same channels.
Step 1: change the host
const pusher = new Pusher("YOUR_PUBLIC_KEY", {
cluster: "thunderclap", // required by pusher-js, not used by Thunderclap
wsHost: "realtime.thunderclap.dev",
forceTLS: true,
enabledTransports: ["ws", "wss"],
channelAuthorization: { endpoint: "/pusher/auth", transport: "ajax" },
});const pusher = new Pusher({
appId: process.env.THUNDERCLAP_APP_ID,
key: process.env.THUNDERCLAP_KEY,
secret: process.env.THUNDERCLAP_SECRET,
host: "realtime.thunderclap.dev",
useTLS: true,
});What keeps working:
- public, private, presence, encrypted and cache channels, and client events (once enabled for the app);
trigger,triggerBatchandsocket_idexclusion;- channel authorization;
- channel queries (
/channels/room-42, and/userson presence channels); - user sign-in with
sendToUserandterminateUserConnections.
Pusher's server libraries for other languages take the same host setting.
Step 2: move to Thunderclap's SDKs
In the browser
Before, with Pusher's client:
const pusher = new Pusher("APP_KEY", {
cluster: "eu",
channelAuthorization: { endpoint: "/pusher/auth", transport: "ajax" },
});
const channel = pusher.subscribe("private-room-42");
channel.bind("message", (data) => show(data));
pusher.connection.bind("state_change", ({ previous, current }) => log(previous, current));After, with thunderclapJS:
const thunderclap = new Thunderclap({
key: process.env.NEXT_PUBLIC_THUNDERCLAP_KEY,
authorize: ({ channel, socketId }) =>
fetch("/realtime/authorize", { method: "POST", body: JSON.stringify({ channel, socketId }) })
.then((r) => r.text()),
});
const channel = thunderclap.subscribe("private-room-42");
channel.bind("message", (data, meta) => show(data)); // meta.seq: the event's sequence number
thunderclap.on("state", ({ previous, current }) => log(previous, current));On your server
Before, with Pusher's server library:
const pusher = new Pusher({ appId, key, secret, cluster: "eu" });
await pusher.trigger("room-42", "message", data, { socket_id: socketId });
// Channel authorization endpoint
const auth = pusher.authorizeChannel(socketId, channelName);
res.send(auth);After, with the server SDK:
const thunderclap = new ThunderclapServer({ appId, secret });
await thunderclap.publish({ channel: "room-42", data, exclude: socketId });
// Channel authorization endpoint: answer with a token for that channel
res.send(await thunderclap.token({ uid: user.id, channels: [channel], ttlSeconds: 300 }));Equivalents
| Pusher | Thunderclap |
|---|---|
App key, app id, secret, cluster | Public key, app id, secret. No cluster: connections go to the nearest location |
channel.bind_global(callback) | channel.bindAll(callback) |
pusher.connection.socket_id | thunderclap.socketId |
Connection states unavailable, disconnected | reconnecting, closed |
trigger(channels, event, data, { socket_id }) | publish({ channel, event, data, exclude }) |
triggerBatch(events) | batch(events), up to 10 events |
authorizeChannel(socketId, channel) | token({ uid, channels: [channel] }), or a connection token granting many channels at once |
authorizeChannel(socketId, channel, { user_id, user_info }) | token({ uid, info, channels }) |
channel.members.each(…), pusher:member_added | channel.members (a Map), member_added on channel.on |
channel.trigger("client-…", data) | channel.trigger("client-…", data) |
Two differences are worth knowing:
- Thunderclap tokens grant channels to a user rather than to one socket, so a connection token can authorize every private channel up front instead of one request per channel.
- Every event carries a sequence number. Where Pusher silently drops events sent while a client was offline, Thunderclap reports the gap (see missed messages).
Webhooks
When you add an endpoint in the dashboard, choose the Pusher-compatible format and your existing webhook handler keeps working unchanged. Once your backend verifies requests with @thunderclapjs/server, switch the endpoint to Thunderclap's format to get event ids, timestamps, member info, and signatures that survive a secret rotation.
In either format, cache_miss is sent once per channel until the next event rather than for every subscriber, and there is no subscription_count webhook.
Not supported
- Listing channels (
pusher.get("/channels")) is refused: query channels by name instead. pusher:subscription_countevents. For who is in a channel, use a presence channel.- User watchlists.
- Transports other than WebSockets: set
enabledTransports: ["ws", "wss"]as shown above.
Pusher is a trademark of its respective owner. Thunderclap is not affiliated with it.