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

Presence channels allow you to track which users are subscribed to a channel, providing real-time visibility into who’s online. Each member can provide user information (name, avatar, etc.) that’s automatically shared with other subscribers. Key Features:
  • Real-time member tracking
  • User information sharing
  • Member join/leave notifications
  • Configurable member limits
  • Automatic cleanup on disconnect

Quick Start

Server Configuration

config/config.json
{
  "presence": {
    "max_members_per_channel": 100,
    "max_member_size_in_kb": 2
  },
  "apps": [
    {
      "id": "my-app",
      "key": "my-key",
      "secret": "my-secret",
      "enable_user_authentication": true
    }
  ]
}

Client Usage

const pusher = new Pusher('my-app-key', {
  cluster: 'mt1',
  authEndpoint: '/pusher/auth'
});

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

// Get current members
channel.bind('pusher:subscription_succeeded', () => {
  console.log('Current members:', channel.members.count);
  
  channel.members.each((member) => {
    console.log('Member:', member.id, member.info);
  });
});

// Listen for new members
channel.bind('pusher:member_added', (member) => {
  console.log('Member joined:', member.id, member.info);
});

// Listen for members leaving
channel.bind('pusher:member_removed', (member) => {
  console.log('Member left:', member.id);
});

Authentication

Presence channels require authentication to associate users with channel members.

Server-Side Authentication

const express = require('express');
const crypto = require('crypto');
const app = express();

app.post('/pusher/auth', (req, res) => {
  const socketId = req.body.socket_id;
  const channel = req.body.channel_name;
  const user = req.session.user; // Your auth system
  
  // Presence channels must start with "presence-"
  if (!channel.startsWith('presence-')) {
    return res.status(403).send('Invalid channel');
  }
  
  // User info that will be shared with other members
  const presenceData = {
    user_id: user.id,
    user_info: {
      name: user.name,
      avatar: user.avatar_url
    }
  };
  
  // Create auth signature
  const stringToSign = `${socketId}:${channel}:${JSON.stringify(presenceData)}`;
  const signature = crypto
    .createHmac('sha256', process.env.PUSHER_SECRET)
    .update(stringToSign)
    .digest('hex');
  
  res.json({
    auth: `${process.env.PUSHER_KEY}:${signature}`,
    channel_data: JSON.stringify(presenceData)
  });
});

Authentication Flow

  1. Client subscribes to presence channel
  2. Client sends auth request to your server (/pusher/auth)
  3. Server validates user and generates auth signature
  4. Server returns auth with user info (user_id, user_info)
  5. Client sends auth to Sockudo
  6. Sockudo validates signature and adds member
  7. Other members notified of new member

Configuration

Global Presence Settings

{
  "presence": {
    "max_members_per_channel": 100,
    "max_member_size_in_kb": 2
  }
}
OptionDefaultDescription
max_members_per_channel100Maximum members per presence channel
max_member_size_in_kb2Maximum size of member info (KB)

Per-App Settings

{
  "apps": [
    {
      "id": "my-app",
      "enable_user_authentication": true,
      "user_authentication_timeout": 3600
    }
  ]
}
OptionDefaultDescription
enable_user_authenticationfalseEnable presence channels
user_authentication_timeout3600Auth timeout (seconds)

Channel Naming

Presence channels must follow naming convention:
TypeFormatExample
Public presencepresence-*presence-lobby
Private presenceprivate-presence-*private-presence-vip-room
Invalid names:
  • presence (no suffix)
  • room-presence (wrong prefix)
  • presence_room (wrong separator)

Member Information

User ID

Required: Unique identifier for the user
const presenceData = {
  user_id: '12345',  // Required, unique
  user_info: { ... }
};
Rules:
  • Must be unique per user
  • String or number
  • Used to deduplicate members

User Info

Optional: Additional information about the user
const presenceData = {
  user_id: '12345',
  user_info: {
    name: 'John Doe',
    avatar: 'https://example.com/avatar.jpg',
    role: 'admin',
    status: 'online'
  }
};
Best practices:
  • Keep info small (<2KB)
  • Only include necessary data
  • Use CDN URLs for images
  • Don’t include sensitive data

Size Limits

Member info is limited by max_member_size_in_kb:
// Good: 0.5KB
{
  name: 'John Doe',
  avatar: 'https://cdn.example.com/avatar.jpg'
}

// Bad: 3KB (exceeds 2KB limit)
{
  name: 'John Doe',
  avatar: 'data:image/png;base64,...',  // Large base64 image
  bio: 'Very long biography...',
  preferences: { ... }  // Large nested object
}

Events

pusher:subscription_succeeded

Triggered when subscription succeeds:
channel.bind('pusher:subscription_succeeded', () => {
  // Access all members
  console.log('Total members:', channel.members.count);
  
  // Get specific member
  const member = channel.members.get('user-123');
  console.log('Member:', member);
  
  // Iterate all members
  channel.members.each((member) => {
    console.log('Member:', member.id, member.info);
  });
});
Payload:
{
  "presence": {
    "ids": ["user-1", "user-2", "user-3"],
    "hash": {
      "user-1": {"name": "Alice", "avatar": "..."},
      "user-2": {"name": "Bob", "avatar": "..."},
      "user-3": {"name": "Charlie", "avatar": "..."}
    },
    "count": 3
  }
}

pusher:member_added

Triggered when a member joins:
channel.bind('pusher:member_added', (member) => {
  console.log('Member joined:', member.id);
  console.log('Member info:', member.info);
  
  // Update UI
  addMemberToList(member);
});
Payload:
{
  "id": "user-123",
  "info": {
    "name": "John Doe",
    "avatar": "https://example.com/avatar.jpg"
  }
}

pusher:member_removed

Triggered when a member leaves:
channel.bind('pusher:member_removed', (member) => {
  console.log('Member left:', member.id);
  
  // Update UI
  removeMemberFromList(member.id);
});
Payload:
{
  "id": "user-123"
}

Use Cases

1. Chat Room

Show who’s online in a chat room:
const channel = pusher.subscribe('presence-chat-room');

// Display initial members
channel.bind('pusher:subscription_succeeded', () => {
  updateMemberList(channel.members);
});

// Add new members
channel.bind('pusher:member_added', (member) => {
  addMember(member);
  showNotification(`${member.info.name} joined`);
});

// Remove members
channel.bind('pusher:member_removed', (member) => {
  removeMember(member.id);
  showNotification(`${member.info.name} left`);
});

function updateMemberList(members) {
  const list = document.getElementById('members');
  list.innerHTML = '';
  
  members.each((member) => {
    const item = document.createElement('li');
    item.innerHTML = `
      <img src="${member.info.avatar}" />
      <span>${member.info.name}</span>
    `;
    list.appendChild(item);
  });
}

2. Collaborative Document Editing

Show who’s editing a document:
const channel = pusher.subscribe(`presence-doc-${docId}`);

channel.bind('pusher:subscription_succeeded', () => {
  // Show all current editors
  const editors = [];
  channel.members.each((member) => {
    editors.push({
      id: member.id,
      name: member.info.name,
      color: assignColor(member.id)
    });
  });
  displayEditors(editors);
});

channel.bind('pusher:member_added', (member) => {
  // Show new editor with assigned color
  const editor = {
    id: member.id,
    name: member.info.name,
    color: assignColor(member.id)
  };
  addEditor(editor);
  showNotification(`${member.info.name} started editing`);
});

channel.bind('pusher:member_removed', (member) => {
  // Remove editor indicator
  removeEditor(member.id);
});

3. Live Video Call

Track participants in a video call:
const channel = pusher.subscribe(`presence-call-${callId}`);

channel.bind('pusher:subscription_succeeded', () => {
  // Initialize video grid with all participants
  channel.members.each((member) => {
    addVideoStream(member.id, member.info);
  });
});

channel.bind('pusher:member_added', (member) => {
  // Add new video stream
  addVideoStream(member.id, member.info);
  playJoinSound();
});

channel.bind('pusher:member_removed', (member) => {
  // Remove video stream
  removeVideoStream(member.id);
  playLeaveSound();
});

4. Gaming Lobby

Show players in a game lobby:
const channel = pusher.subscribe(`presence-lobby-${lobbyId}`);

channel.bind('pusher:subscription_succeeded', () => {
  // Display all players
  updatePlayerList(channel.members);
  
  // Check if lobby is full
  if (channel.members.count >= MAX_PLAYERS) {
    enableStartButton();
  }
});

channel.bind('pusher:member_added', (member) => {
  // Add player to list
  addPlayer({
    id: member.id,
    name: member.info.name,
    level: member.info.level,
    avatar: member.info.avatar
  });
  
  // Check if lobby is now full
  if (channel.members.count >= MAX_PLAYERS) {
    enableStartButton();
  }
});

channel.bind('pusher:member_removed', (member) => {
  removePlayer(member.id);
  disableStartButton();
});

Best Practices

1. Keep Member Info Small

Only include necessary information:
// Good: 200 bytes
{
  user_id: '123',
  user_info: {
    name: 'John',
    avatar: 'https://cdn.example.com/123.jpg'
  }
}

// Bad: 2KB
{
  user_id: '123',
  user_info: {
    name: 'John',
    avatar: 'data:image/...',  // Large base64
    bio: '...',                 // Unnecessary
    preferences: { ... }        // Unnecessary
  }
}

2. Handle Disconnections Gracefully

Users may disconnect unexpectedly:
channel.bind('pusher:member_removed', (member) => {
  // Don't show "left" notification immediately
  // User might be reconnecting
  
  setTimeout(() => {
    if (!channel.members.get(member.id)) {
      showNotification(`${member.info.name} left`);
    }
  }, 5000);  // Wait 5 seconds
});

3. Limit Channel Size

Set appropriate limits:
{
  "presence": {
    "max_members_per_channel": 100  // Adjust based on use case
  }
}
Use CaseRecommended Limit
Chat room100-500
Video call10-50
Collaborative editing10-20
Gaming lobby4-100

4. Implement User Deduplication

Prevent duplicate members:
app.post('/pusher/auth', (req, res) => {
  const user = req.session.user;
  
  // Use consistent user_id
  const presenceData = {
    user_id: `user-${user.id}`,  // Same format always
    user_info: { ... }
  };
  
  // ...
});

5. Validate Member Limits

Check limits before allowing join:
channel.bind('pusher:subscription_error', (error) => {
  if (error.type === 'PresenceLimitReached') {
    showError('Room is full, please try again later');
  }
});

Troubleshooting

Members Not Showing Up

Check 1: Is user authentication enabled?
{
  "apps": [{
    "enable_user_authentication": true
  }]
}
Check 2: Is auth endpoint configured?
const pusher = new Pusher('key', {
  authEndpoint: '/pusher/auth'  // Must be set
});
Check 3: Is auth signature correct?
// Verify signature format
const stringToSign = `${socketId}:${channel}:${JSON.stringify(presenceData)}`;
const signature = crypto
  .createHmac('sha256', secret)
  .update(stringToSign)
  .digest('hex');

Channel Limit Reached

Symptom: New members can’t join Check limit:
grep "max_members_per_channel" config/config.json
Increase limit:
{
  "presence": {
    "max_members_per_channel": 500  // Increase from 100
  }
}

Member Info Too Large

Symptom: Subscription fails with size error Check size:
const presenceData = { user_id: '123', user_info: {...} };
const size = JSON.stringify(presenceData).length;
console.log('Size:', size, 'bytes');
Reduce size:
// Before: 3KB
user_info: {
  avatar: 'data:image/png;base64,...',  // Remove
  bio: 'Long text...'                   // Remove
}

// After: 200 bytes
user_info: {
  avatar: 'https://cdn.example.com/123.jpg'  // CDN URL
}

Members Not Leaving

Symptom: Members stay in channel after disconnect Check 1: Is connection closed properly?
window.addEventListener('beforeunload', () => {
  pusher.disconnect();
});
Check 2: Check activity timeout:
{
  "activity_timeout": 120  // Seconds until disconnect
}

Migration Guide

Enabling Presence Channels

1. Enable user authentication:
{
  "apps": [{
    "enable_user_authentication": true
  }]
}
2. Implement auth endpoint:
app.post('/pusher/auth', (req, res) => {
  // Implement authentication (see above)
});
3. Update client to use presence channels:
const channel = pusher.subscribe('presence-room');
4. Handle presence events:
channel.bind('pusher:member_added', (member) => {
  console.log('Member joined:', member);
});

Next Steps

Webhooks

Set up webhooks to track presence events

Tag Filtering

Filter messages with server-side tag filtering