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:
| Type | Direction | Prefix |
|---|---|---|
| Server events | Your server → subscribers | Any name (your choice) |
| System events | Soketify → your client | pusher: |
| Client events | Client → other subscribers | client- |
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
// 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 sideNaming 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
| Limit | Value |
|---|---|
| Event payload size | 100 KB – 4 MB |
| Event name length | 200 characters |
| Channels per trigger call | 100 |
| 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.
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.
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.
1pusher.bind_global((eventName, data) => {
2 console.log(`[${eventName}]`, data);
3});
4. Unbinding
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.
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.
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.
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.
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.
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.
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
1await pusher.trigger("orders", "order-shipped", {
2 orderId: "ORD-1234",
3 trackingNumber: "1Z999AA10123456784",
4});
Multiple channels at once
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)
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
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-orpresence- - Trigger only after
pusher:subscription_succeededfires - 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
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
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 Event | Where | When it fires |
|---|---|---|
pusher:subscription_succeeded | All channels | Subscription confirmed |
pusher:subscription_error | Private, presence | Auth endpoint rejected the request |
pusher:member_added | Presence only | A user joins the channel |
pusher:member_removed | Presence only | A user leaves the channel |
pusher:cache_miss | Cache channels | Subscribe when no cached event exists |
pusher:subscription_count | All 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 happens | Messages |
|---|---|
| You publish an event over the HTTP API | 1 |
| That event is delivered to a subscriber | 1 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_count | 1 extra |
| Protocol traffic: ping and pong, connection_established, subscribing and its confirmation, member_added, member_removed, subscription_count, errors | 0 |
| A webhook we deliver to your server | 0 |
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
- Set up authentication to enable private and presence channels
- Configure webhooks to receive server-side callbacks on channel and member events
- Review all channel types to choose the right one for your use case
- API Reference for triggering events via the REST API