Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/sockudo/sockudo/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Client events allow clients to send messages directly to other clients subscribed to the same channel, without going through your backend server.
Client events are only supported on private and presence channels for security reasons.

Sending Client Events

To send a client event, publish a message over the WebSocket with an event name prefixed with client-:
{
  "event": "client-typing",
  "channel": "private-chat-123",
  "data": "{\"user\":\"Alice\",\"text\":\"Hello...\"}"
}
event
string
required
Event name starting with client- (e.g., client-typing, client-cursor-move)
channel
string
required
The private or presence channel to send the event to
data
string
required
Event payload as a JSON-encoded string (max size depends on server configuration)

Restrictions

Channel Type Restriction

Client events can only be sent on:
  • Private channels (prefix: private-)
  • Presence channels (prefix: presence-)
Attempting to send client events on public channels will fail.

Event Name Prefix

Client event names must start with client-. Examples:
  • client-typing
  • client-mouse-move
  • client-reaction
  • typing (missing prefix)
  • my-client-event (prefix not at start)

Subscription Requirement

You can only send client events to channels you’re currently subscribed to.

Event Broadcasting

When a client sends a client event:
  1. The event is sent to the server over the WebSocket
  2. The server broadcasts it to all other subscribers on that channel
  3. The sender does NOT receive their own event (to avoid echo)

Example Flow

// Client A sends a typing indicator
privateChannel.trigger('client-typing', {
  user: 'Alice',
  typing: true
});

// Client B (and all other subscribers) receive it
privateChannel.bind('client-typing', (data) => {
  console.log(`${data.user} is typing...`);
});

// Client A does NOT receive their own event

Message Structure

Client events follow the same structure as regular events:
{
  "event": "client-typing",
  "channel": "private-chat-123",
  "data": "{\"user\":\"Alice\",\"typing\":true}"
}
event
string
The client event name (with client- prefix)
channel
string
The channel the event was sent on
data
string
JSON-encoded event payload

Use Cases

Client events are ideal for:

1. Typing Indicators

const chatChannel = pusher.subscribe('private-chat-room');

// Send typing indicator
chatInput.addEventListener('input', () => {
  chatChannel.trigger('client-typing', {
    user: currentUser.name,
    typing: true
  });
});

// Receive typing indicators
chatChannel.bind('client-typing', (data) => {
  showTypingIndicator(data.user);
});

2. Cursor Position Sharing

const docChannel = pusher.subscribe('presence-document-123');

// Send cursor position
document.addEventListener('mousemove', (e) => {
  docChannel.trigger('client-cursor', {
    user: currentUser.id,
    x: e.clientX,
    y: e.clientY
  });
});

// Receive other users' cursors
docChannel.bind('client-cursor', (data) => {
  updateCursor(data.user, data.x, data.y);
});

3. Reactions / Emojis

const streamChannel = pusher.subscribe('presence-livestream');

// Send reaction
reactionButton.addEventListener('click', () => {
  streamChannel.trigger('client-reaction', {
    emoji: '👍',
    user: currentUser.name
  });
});

// Receive reactions
streamChannel.bind('client-reaction', (data) => {
  showFloatingEmoji(data.emoji, data.user);
});

4. Collaborative Editing

const editorChannel = pusher.subscribe('private-editor-session');

// Send text changes
editor.on('change', (delta) => {
  editorChannel.trigger('client-edit', {
    user: currentUser.id,
    delta: delta,
    timestamp: Date.now()
  });
});

// Receive remote edits
editorChannel.bind('client-edit', (data) => {
  if (data.user !== currentUser.id) {
    editor.applyDelta(data.delta);
  }
});

Example: pusher-js

import Pusher from 'pusher-js';

const pusher = new Pusher('your-app-key', {
  wsHost: 'localhost',
  wsPort: 6001,
  forceTLS: false,
  authEndpoint: '/pusher/auth'
});

// Subscribe to private channel
const channel = pusher.subscribe('private-chat-room');

channel.bind('pusher:subscription_succeeded', () => {
  // Send client event
  channel.trigger('client-user-joined', {
    user: 'Alice',
    timestamp: Date.now()
  });
});

// Receive client events from other users
channel.bind('client-user-joined', (data) => {
  console.log(`${data.user} joined at ${new Date(data.timestamp)}`);
});

channel.bind('client-typing', (data) => {
  console.log(`${data.user} is typing...`);
});

Raw WebSocket Example

const ws = new WebSocket('ws://localhost:6001/app/your-app-key');

ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  
  if (message.event === 'pusher:connection_established') {
    const data = JSON.parse(message.data);
    const socketId = data.socket_id;
    
    // Subscribe to private channel (with auth)
    ws.send(JSON.stringify({
      event: 'pusher:subscribe',
      data: {
        channel: 'private-chat',
        auth: 'app-key:signature' // Get from your auth endpoint
      }
    }));
  }
  
  if (message.event === 'pusher_internal:subscription_succeeded') {
    // Send client event
    ws.send(JSON.stringify({
      event: 'client-hello',
      channel: 'private-chat',
      data: JSON.stringify({
        message: 'Hi everyone!',
        user: 'Alice'
      })
    }));
  }
  
  // Receive client events
  if (message.event.startsWith('client-')) {
    const payload = JSON.parse(message.data);
    console.log('Client event:', message.event, payload);
  }
};

Rate Limiting

Client events may be rate-limited by the server to prevent abuse. Check your server configuration for limits on:
  • Events per second per connection
  • Message size limits
  • Maximum subscribed channels per connection

Best Practices

1. Keep Payloads Small

Client events are sent to all subscribers. Keep data minimal:
// ❌ Bad: Sending large data
channel.trigger('client-update', {
  fullDocument: largeObject,
  history: allChanges
});

// ✅ Good: Sending only what's needed
channel.trigger('client-update', {
  field: 'title',
  value: 'New Title',
  user: userId
});

2. Throttle High-Frequency Events

For events like mouse movement, throttle the rate:
import { throttle } from 'lodash';

const sendCursor = throttle((x, y) => {
  channel.trigger('client-cursor', { x, y, user: userId });
}, 100); // Max 10 events per second

document.addEventListener('mousemove', (e) => {
  sendCursor(e.clientX, e.clientY);
});

3. Use Presence Channels for User Context

Combine client events with presence channels to know who’s online:
const presenceChannel = pusher.subscribe('presence-editor');

presenceChannel.bind('pusher:subscription_succeeded', (members) => {
  members.each((member) => {
    console.log('Online:', member.id, member.info);
  });
});

presenceChannel.bind('client-edit', (data) => {
  // You know who sent this because they're in the presence channel
  const member = presenceChannel.members.get(data.user);
  console.log(`Edit from ${member.info.name}`);
});

4. Handle Echo Prevention

Remember: you don’t receive your own events. Handle local updates separately:
function updateEditor(change, isLocal = false) {
  // Apply change locally
  editor.applyChange(change);
  
  // Broadcast to others (only if local change)
  if (isLocal) {
    channel.trigger('client-edit', change);
  }
}

// Local edit
editor.on('change', (change) => {
  updateEditor(change, true);
});

// Remote edit
channel.bind('client-edit', (change) => {
  updateEditor(change, false);
});

Error Handling

Client events can fail for several reasons:
// pusher-js doesn't provide feedback for client events
// But you can detect common issues:

channel.bind('pusher:subscription_error', (err) => {
  console.error('Cannot subscribe, client events unavailable');
});

// Check subscription state before sending
if (channel.subscribed) {
  channel.trigger('client-typing', data);
} else {
  console.warn('Not subscribed yet');
}

Security Considerations

  1. Always use private/presence channels - Client events are disabled on public channels
  2. Validate on the client - Since events bypass your backend, validate data on receiving clients
  3. Rate limit on server - Configure rate limits to prevent spam
  4. Don’t trust client data - Treat client events as untrusted user input
  5. Use authentication - Ensure only authorized users can join the channel

Comparison with Server Events

FeatureClient EventsServer Events
SourceClient-to-clientServer-to-client
ChannelsPrivate/Presence onlyAll channel types
ValidationClient-side onlyServer-side
Prefixclient- requiredAny name
EchoSender doesn’t receiveAll subscribers receive
Use CaseReal-time collaborationApplication events