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

pusher-js
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" },
});
pusher
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, triggerBatch and socket_id exclusion;
  • channel authorization;
  • channel queries (/channels/room-42, and /users on presence channels);
  • user sign-in with sendToUser and terminateUserConnections.

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:

pusher-js
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:

@thunderclapjs/client
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:

pusher
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:

@thunderclapjs/server
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

PusherThunderclap
App key, app id, secret, clusterPublic key, app id, secret. No cluster: connections go to the nearest location
channel.bind_global(callback)channel.bindAll(callback)
pusher.connection.socket_idthunderclap.socketId
Connection states unavailable, disconnectedreconnecting, 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_addedchannel.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_count events. 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.