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 name starting with client- (e.g., client-typing, client-cursor-move)
The private or presence channel to send the event to
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:
- The event is sent to the server over the WebSocket
- The server broadcasts it to all other subscribers on that channel
- 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}"
}
The client event name (with client- prefix)
The channel the event was sent on
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
- Always use private/presence channels - Client events are disabled on public channels
- Validate on the client - Since events bypass your backend, validate data on receiving clients
- Rate limit on server - Configure rate limits to prevent spam
- Don’t trust client data - Treat client events as untrusted user input
- Use authentication - Ensure only authorized users can join the channel
Comparison with Server Events
| Feature | Client Events | Server Events |
|---|
| Source | Client-to-client | Server-to-client |
| Channels | Private/Presence only | All channel types |
| Validation | Client-side only | Server-side |
| Prefix | client- required | Any name |
| Echo | Sender doesn’t receive | All subscribers receive |
| Use Case | Real-time collaboration | Application events |