Events

Events are the messages that flow through channels. Understanding how to send, receive, and respond to them, including the system events Soketify fires automatically, is the core of building anything real-time.

What is an Event?

An event is a named message sent on a channel. It has three parts:

  • Channel: where the event is sent (e.g., private-user-42)
  • Name: what happened (e.g., order-shipped)
  • Data: the JSON payload (e.g., { orderId: "ORD-1234" })

Events travel in one of three directions:

Your Server
Soketify
Client A
Client B
trigger event (server event)
order-shipped
order-shipped
pusher:subscription_succeeded (system event)
client-typing (client event)
client-typing
TypeDirectionPrefix
Server eventsYour server → subscribersAny name (your choice)
System eventsSoketify → your clientpusher:
Client eventsClient → other subscribersclient-

Event Naming Rules

  • Maximum 200 characters
  • Must not start with pusher:: that prefix is reserved for system events
  • Client events must start with client-
  • Everything else is valid: hyphens, underscores, dots, mixed case
Event naming
// Good event names
"order-shipped"
"message.created"
"user_status_changed"
"PRICE_UPDATE"
"payment:completed"    // custom namespacing is fine

// Invalid
"pusher:my-event"      // reserved prefix
"client-typing"        // only valid on private/presence channels
                       // and only from the client side

Naming Convention

Name events in past tense to describe what happened: order-shipped, message-created, user-joined. This makes your event log read like a timeline, which is exactly what it is.

Data Limits

LimitValue
Event payload size100 KB – 4 MB
Event name length200 characters
Channels per trigger call100
Events per second (server or client)No limit

Event payload limits

Each plan includes a per-event payload limit: from 100 KB (Free) up to 4 MB (Scale-2). That's 10–400x larger than Pusher's default 10 KB. For very large data, store it elsewhere and send a reference in the event.

Binding to Events

There are four ways to bind to events, depending on how broadly you want to listen.

1. Channel-level binding

The most common pattern. Only fires for events on that specific channel.

channel-bind.js
javascript
1const channel = pusher.subscribe("orders");
2
3channel.bind("order-shipped", (data) => {
4  console.log("Order shipped:", data.orderId);
5});
6
7// Optional context object as third argument
8const context = { component: this };
9channel.bind("order-shipped", handler, context);

2. Global client binding

Fires whenever that event name appears on any subscribed channel. Useful for cross-channel listeners.

global-bind.js
javascript
1// Fires when "notification" arrives on ANY subscribed channel
2pusher.bind("notification", (data) => {
3  showToast(data.message);
4});

3. Catch-all binding

Fires for every event on every subscribed channel. Useful for debugging or logging.

bind-global.js
javascript
1pusher.bind_global((eventName, data) => {
2  console.log(`[${eventName}]`, data);
3});

4. Unbinding

unbind.js
javascript
1// Remove a specific callback
2channel.unbind("order-shipped", myHandler);
3
4// Remove all callbacks for an event name
5channel.unbind("order-shipped");
6
7// Remove all callbacks on a channel
8channel.unbind_all();
9
10// Remove a global binding
11pusher.unbind("notification", myHandler);

System Events

Soketify fires system events with the pusher: prefix. You cannot trigger these yourself: they are how Soketify tells your client what is happening with the connection and subscriptions.

pusher:subscription_succeeded

Fires when a channel subscription is confirmed. For presence channels it carries the full member list. For every other channel type the payload is an empty object, exactly as on Pusher.

subscription-succeeded.js
javascript
1// Public or private channel
2channel.bind("pusher:subscription_succeeded", () => {
3  console.log("Subscribed!");
4  // Safe to trigger client events from here
5});
6
7// Presence channel (includes member data)
8presenceChannel.bind("pusher:subscription_succeeded", (members) => {
9  console.log("Members online:", members.count);
10  members.each((member) => {
11    addToUserList(member.id, member.info);
12  });
13});

pusher:subscription_error

Fires when a private or presence channel subscription fails, usually because your auth endpoint returned a non-200 response.

subscription-error.js
javascript
1channel.bind("pusher:subscription_error", (error) => {
2  console.error("Subscription failed:", error.status, error.error);
3  // error.status: HTTP status from your auth endpoint (e.g., 403)
4  // error.type:   "AuthError" or "WebSocketError"
5  // error.error:  human-readable message
6});

pusher:member_added / pusher:member_removed

Presence-channel-only. Fires when a user joins or leaves.

member-events.js
javascript
1const channel = pusher.subscribe("presence-chat-room");
2
3channel.bind("pusher:member_added", (member) => {
4  console.log(`${member.info.name} joined`);
5  // member.id: the user_id from your auth endpoint
6  // member.info: the user_info object from your auth endpoint
7});
8
9channel.bind("pusher:member_removed", (member) => {
10  console.log(`${member.info.name} left`);
11});

pusher:cache_miss

Cache-channel-only. Fires when you subscribe to a cache channel and there is no cached event yet. Use this to trigger an initial data load.

cache-miss.js
javascript
1const channel = pusher.subscribe("cache-live-score");
2
3channel.bind("score-update", (data) => {
4  updateScoreboard(data);
5});
6
7channel.bind("pusher:cache_miss", () => {
8  // No cached value; request the current score from your server
9  fetch("/api/current-score")
10    .then((r) => r.json())
11    .then((data) => updateScoreboard(data));
12});

pusher:subscription_count

Fires when the subscriber count changes on a channel (must be enabled in app settings). On channels with 100+ subscribers, fires at most every 5 seconds.

subscription-count.js
javascript
1channel.bind("pusher:subscription_count", (data) => {
2  viewerCountEl.textContent = `${data.subscription_count} watching`;
3});

pusher:error (connection-level)

Fires on the connection object (not a channel) when a connection-level error occurs. See the Connection guide for error codes.

connection-error.js
javascript
1// Note: bind on pusher.connection, not on a channel
2pusher.connection.bind("error", (err) => {
3  console.error("Connection error:", err.error.data.code);
4});

Triggering Server Events

Your server triggers events through the REST API. This is the primary way to push data to your clients.

Single channel, single event

trigger-single.js
javascript
1await pusher.trigger("orders", "order-shipped", {
2  orderId: "ORD-1234",
3  trackingNumber: "1Z999AA10123456784",
4});

Multiple channels at once

trigger-multi.js
javascript
1// Same event to up to 100 channels in one API call
2await pusher.trigger(
3  ["private-user-1", "private-user-2", "private-user-3"],
4  "notification",
5  { message: "System maintenance in 10 minutes." }
6);

Excluding the sender (echo prevention)

echo-prevention.js
javascript
1// Client sends the action + its socket_id
2// POST /api/messages  { text: "Hello", socket_id: "12345.67890" }
3
4app.post("/api/messages", async (req, res) => {
5  const { text, socket_id } = req.body;
6  await db.messages.create({ text });
7
8  // Trigger to all subscribers except the sender
9  await pusher.trigger("chat-general", "message-created", { text }, {
10    socket_id,
11  });
12
13  res.json({ ok: true });
14});

Batch triggering different events

batch-trigger.js
javascript
1// Up to 10 different events per batch call
2await pusher.triggerBatch([
3  {
4    channel: "private-user-1",
5    name: "order-shipped",
6    data: { orderId: "ORD-001" },
7  },
8  {
9    channel: "private-user-2",
10    name: "order-delivered",
11    data: { orderId: "ORD-002" },
12  },
13  {
14    channel: "admin-alerts",
15    name: "bulk-dispatch-completed",
16    data: { count: 2 },
17  },
18]);

See the API Reference for the raw HTTP endpoints and authentication details.

Client Events

Client events are sent directly from one connected browser to the other subscribers of the same channel, without a round-trip to your server. They must be prefixed with client- and are only available on private and presence channels.

Requirements

  • Client events must be enabled in your app’s Dashboard settings
  • Channel must be private- or presence-
  • Trigger only after pusher:subscription_succeeded fires
  • No per-second rate limit: Soketify does not meter events by rate, only against your monthly allowance
  • The sender does not receive its own client events
  • Payload limit depends on your plan (100 KB – 4 MB)

Typing Indicators Example

typing-indicator.js
javascript
1const channel = pusher.subscribe("private-chat-42");
2
3channel.bind("pusher:subscription_succeeded", () => {
4  let typingTimeout: ReturnType<typeof setTimeout>;
5
6  messageInput.addEventListener("input", () => {
7    channel.trigger("client-typing", { isTyping: true });
8
9    // Clear the "stopped typing" timer and restart it
10    clearTimeout(typingTimeout);
11    typingTimeout = setTimeout(() => {
12      channel.trigger("client-typing", { isTyping: false });
13    }, 1500);
14  });
15});
16
17// Receive typing events from the other party
18channel.bind("client-typing", (data) => {
19  typingIndicator.hidden = !data.isTyping;
20});

Collaborative Cursor Positions

cursor-tracking.js
javascript
1const channel = pusher.subscribe("presence-doc-abc123");
2
3channel.bind("pusher:subscription_succeeded", () => {
4  // No rate limit to stay under, but every frame is a billed message:
5  // one cursor sample every 100ms is plenty for a smooth cursor.
6  let lastSent = 0;
7
8  document.addEventListener("mousemove", (e) => {
9    const now = Date.now();
10    if (now - lastSent < 100) return; // 10 samples/sec is enough
11    lastSent = now;
12
13    channel.trigger("client-cursor-moved", {
14      x: Math.round(e.clientX),
15      y: Math.round(e.clientY),
16    });
17  });
18});
19
20// Render other users' cursors
21channel.bind("client-cursor-moved", (data, metadata) => {
22  updateCursor(metadata.user_id, data.x, data.y);
23  // Use metadata.user_id (signed by Soketify, not self-reported)
24});

Use metadata.user_id on presence channels

On presence channels, the callback receives a second argument, metadata, with a user_id field set by Soketify from the auth endpoint. This is tamper-proof. If you embed a user ID in the event data payload, any client can fake it, so always use metadata.user_id for identity.

Quick Reference

System EventWhereWhen it fires
pusher:subscription_succeededAll channelsSubscription confirmed
pusher:subscription_errorPrivate, presenceAuth endpoint rejected the request
pusher:member_addedPresence onlyA user joins the channel
pusher:member_removedPresence onlyA user leaves the channel
pusher:cache_missCache channelsSubscribe when no cached event exists
pusher:subscription_countAll channels (if enabled)Subscriber count changes

How messages are counted

Soketify counts messages the same way Pusher does, so an estimate you made against a Pusher bill carries across unchanged. A message is an application event and every copy of it that reaches a subscriber; the traffic the protocol needs in order to work is not counted at all.

What happensMessages
You publish an event over the HTTP API1
That event is delivered to a subscriber1 per subscriber
A client sends a client event (client-*)1, plus 1 per subscriber it reaches
Any other HTTP API call (/channels, /users)1
A publish that also asks for info=subscription_count or user_count1 extra
Protocol traffic: ping and pong, connection_established, subscribing and its confirmation, member_added, member_removed, subscription_count, errors0
A webhook we deliver to your server0

So one event published to a channel with 100 subscribers counts as 101: the API call, plus the 100 deliveries. A connection that is open but idle costs nothing, however long it stays open, and neither does subscribing to a channel.

Fan-out is what moves the number

The size of your bill follows how many clients receive each event, not how many you send. Subscribing clients to narrower channels is the single change that makes the biggest difference; for large audiences, subscription_count events exist precisely so a busy channel does not have to broadcast its own counter.

Next Steps