Repository navigation
presence
Ferdi ÜNAL edited this page Jan 25, 2026
·
3 revisions
Real-time user presence tracking with soketi.rs and Pusher SDK
- Overview
- Prerequisites
- Setup Instructions
- Server-Side Authentication
- TypeScript Implementation
- JavaScript Implementation
- Expected Output
- Advanced Features
Presence channels allow you to track which users are currently subscribed to a channel. This is perfect for features like:
- Showing who's online in a chat room
- Displaying active users in a collaborative document
- Building multiplayer game lobbies
- Creating "who's viewing" indicators
Unlike public channels, presence channels require authentication and provide member information to all subscribers.
- Node.js 18+ installed
- soketi.rs server running (see Getting Started)
- Express.js or similar backend framework for authentication
- Basic knowledge of JavaScript/TypeScript
- Understanding of Basic Chat Example
Client-side:
npm install pusher-jsServer-side:
npm install express pusher cors body-parserStart soketi.rs with client events enabled:
soketi start \
--port=6001 \
--app-id=app-id \
--key=app-key \
--secret=app-secret \
--enable-client-messagesCreate an presence.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Presence Channel - soketi.rs</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.container {
display: flex;
gap: 20px;
}
.chat-section {
flex: 2;
}
.users-section {
flex: 1;
border-left: 2px solid #ccc;
padding-left: 20px;
}
#messages {
border: 1px solid #ccc;
height: 400px;
overflow-y: auto;
padding: 10px;
margin-bottom: 10px;
background: #f9f9f9;
}
.message {
margin: 5px 0;
padding: 8px;
background: white;
border-radius: 3px;
}
.message-user {
font-weight: bold;
color: #0066cc;
}
.system-message {
color: #666;
font-style: italic;
}
#users-list {
list-style: none;
padding: 0;
}
.user-item {
padding: 10px;
margin: 5px 0;
background: #f0f0f0;
border-radius: 3px;
display: flex;
align-items: center;
gap: 10px;
}
.user-status {
width: 10px;
height: 10px;
background: #28a745;
border-radius: 50%;
}
.user-info {
flex: 1;
}
.user-name {
font-weight: bold;
}
.user-email {
font-size: 0.85em;
color: #666;
}
#input-container {
display: flex;
gap: 10px;
}
#message-input {
flex: 1;
padding: 10px;
border: 1px solid #ccc;
border-radius: 3px;
}
button {
padding: 10px 20px;
background: #0066cc;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
button:hover {
background: #0052a3;
}
#status {
padding: 10px;
margin-bottom: 10px;
border-radius: 3px;
text-align: center;
}
.connected {
background: #d4edda;
color: #155724;
}
.disconnected {
background: #f8d7da;
color: #721c24;
}
.member-count {
font-size: 0.9em;
color: #666;
margin-bottom: 10px;
}
</style>
</head>
<body>
<h1>Presence Channel Demo</h1>
<div id="status" class="disconnected">Disconnected</div>
<div class="container">
<div class="chat-section">
<h2>Chat</h2>
<div id="messages"></div>
<div id="input-container">
<input type="text" id="message-input" placeholder="Type your message..." />
<button id="send-button">Send</button>
</div>
</div>
<div class="users-section">
<h2>Online Users</h2>
<div class="member-count">
<span id="member-count">0</span> users online
</div>
<ul id="users-list"></ul>
</div>
</div>
<script src="https://js.pusher.com/8.2.0/pusher.min.js"></script>
<script src="presence.js"></script>
</body>
</html>Create an auth-server.ts file:
import express, { Request, Response } from 'express';
import Pusher from 'pusher';
import cors from 'cors';
import bodyParser from 'body-parser';
const app = express();
// Middleware
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// Initialize Pusher server SDK
const pusher = new Pusher({
appId: 'app-id',
key: 'app-key',
secret: 'app-secret',
cluster: 'mt1',
host: 'localhost',
port: 6001,
useTLS: false,
});
// Mock user database (replace with real authentication)
interface User {
id: string;
name: string;
email: string;
}
const users: Record<string, User> = {
'user-1': { id: 'user-1', name: 'Alice Johnson', email: 'alice@example.com' },
'user-2': { id: 'user-2', name: 'Bob Smith', email: 'bob@example.com' },
'user-3': { id: 'user-3', name: 'Charlie Brown', email: 'charlie@example.com' },
};
// Authentication endpoint for presence channels
app.post('/pusher/auth', (req: Request, res: Response) => {
const socketId = req.body.socket_id;
const channelName = req.body.channel_name;
// Get user from session/token (simplified for demo)
const userId = req.query.user_id as string || 'user-1';
const user = users[userId];
if (!user) {
return res.status(403).json({ error: 'User not found' });
}
// Only authenticate presence channels
if (!channelName.startsWith('presence-')) {
return res.status(403).json({ error: 'Invalid channel' });
}
// Presence data that will be shared with other users
const presenceData = {
user_id: user.id,
user_info: {
name: user.name,
email: user.email,
},
};
try {
const authResponse = pusher.authorizeChannel(socketId, channelName, presenceData);
res.json(authResponse);
} catch (error) {
console.error('Auth error:', error);
res.status(500).json({ error: 'Authentication failed' });
}
});
// Start server
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Auth server running on http://localhost:${PORT}`);
});Create an auth-server.js file:
const express = require('express');
const Pusher = require('pusher');
const cors = require('cors');
const bodyParser = require('body-parser');
const app = express();
// Middleware
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// Initialize Pusher server SDK
const pusher = new Pusher({
appId: 'app-id',
key: 'app-key',
secret: 'app-secret',
cluster: 'mt1',
host: 'localhost',
port: 6001,
useTLS: false,
});
// Mock user database (replace with real authentication)
const users = {
'user-1': { id: 'user-1', name: 'Alice Johnson', email: 'alice@example.com' },
'user-2': { id: 'user-2', name: 'Bob Smith', email: 'bob@example.com' },
'user-3': { id: 'user-3', name: 'Charlie Brown', email: 'charlie@example.com' },
};
// Authentication endpoint for presence channels
app.post('/pusher/auth', (req, res) => {
const socketId = req.body.socket_id;
const channelName = req.body.channel_name;
// Get user from session/token (simplified for demo)
const userId = req.query.user_id || 'user-1';
const user = users[userId];
if (!user) {
return res.status(403).json({ error: 'User not found' });
}
// Only authenticate presence channels
if (!channelName.startsWith('presence-')) {
return res.status(403).json({ error: 'Invalid channel' });
}
// Presence data that will be shared with other users
const presenceData = {
user_id: user.id,
user_info: {
name: user.name,
email: user.email,
},
};
try {
const authResponse = pusher.authorizeChannel(socketId, channelName, presenceData);
res.json(authResponse);
} catch (error) {
console.error('Auth error:', error);
res.status(500).json({ error: 'Authentication failed' });
}
});
// Start server
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Auth server running on http://localhost:${PORT}`);
});Create a presence.ts file:
import Pusher, { PresenceChannel } from 'pusher-js';
// Configuration
interface PresenceMember {
id: string;
info: {
name: string;
email: string;
};
}
interface Message {
user: string;
text: string;
timestamp: string;
}
// Simulate user ID (in production, get from authentication)
const currentUserId = `user-${Math.floor(Math.random() * 3) + 1}`;
// Initialize Pusher client
const pusher = new Pusher('app-key', {
wsHost: 'localhost',
wsPort: 6001,
forceTLS: false,
enabledTransports: ['ws', 'wss'],
cluster: 'mt1',
disableStats: true,
authEndpoint: `http://localhost:3000/pusher/auth?user_id=${currentUserId}`,
});
// Subscribe to presence channel
const channel = pusher.subscribe('presence-chat-room') as PresenceChannel;
// DOM elements
const messagesDiv = document.getElementById('messages') as HTMLDivElement;
const messageInput = document.getElementById('message-input') as HTMLInputElement;
const sendButton = document.getElementById('send-button') as HTMLButtonElement;
const statusDiv = document.getElementById('status') as HTMLDivElement;
const usersListDiv = document.getElementById('users-list') as HTMLUListElement;
const memberCountSpan = document.getElementById('member-count') as HTMLSpanElement;
// Connection state handlers
pusher.connection.bind('connected', () => {
console.log('Connected to soketi.rs');
statusDiv.textContent = 'Connected';
statusDiv.className = 'connected';
});
pusher.connection.bind('disconnected', () => {
console.log('Disconnected from soketi.rs');
statusDiv.textContent = 'Disconnected';
statusDiv.className = 'disconnected';
});
// Presence channel events
channel.bind('pusher:subscription_succeeded', (members: any) => {
console.log('Successfully subscribed to presence channel');
updateUsersList(members);
displaySystemMessage(`You joined the room as ${members.me.info.name}`);
});
channel.bind('pusher:member_added', (member: PresenceMember) => {
console.log('Member added:', member);
addUserToList(member);
displaySystemMessage(`${member.info.name} joined the room`);
});
channel.bind('pusher:member_removed', (member: PresenceMember) => {
console.log('Member removed:', member);
removeUserFromList(member.id);
displaySystemMessage(`${member.info.name} left the room`);
});
// Chat message event
channel.bind('client-message', (data: Message) => {
displayMessage(data);
});
// Update users list
function updateUsersList(members: any): void {
usersListDiv.innerHTML = '';
memberCountSpan.textContent = members.count.toString();
members.each((member: PresenceMember) => {
addUserToList(member);
});
}
// Add user to list
function addUserToList(member: PresenceMember): void {
const existingUser = document.getElementById(`user-${member.id}`);
if (existingUser) return;
const userEl = document.createElement('li');
userEl.id = `user-${member.id}`;
userEl.className = 'user-item';
userEl.innerHTML = `
<div class="user-status"></div>
<div class="user-info">
<div class="user-name">${member.info.name}</div>
<div class="user-email">${member.info.email}</div>
</div>
`;
usersListDiv.appendChild(userEl);
updateMemberCount();
}
// Remove user from list
function removeUserFromList(userId: string): void {
const userEl = document.getElementById(`user-${userId}`);
if (userEl) {
userEl.remove();
updateMemberCount();
}
}
// Update member count
function updateMemberCount(): void {
const count = usersListDiv.children.length;
memberCountSpan.textContent = count.toString();
}
// Display chat message
function displayMessage(data: Message): void {
const messageEl = document.createElement('div');
messageEl.className = 'message';
const time = new Date(data.timestamp).toLocaleTimeString();
messageEl.innerHTML = `
<span class="message-user">${data.user}:</span>
<span class="message-text">${escapeHtml(data.text)}</span>
<span class="message-time">${time}</span>
`;
messagesDiv.appendChild(messageEl);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
// Display system message
function displaySystemMessage(text: string): void {
const messageEl = document.createElement('div');
messageEl.className = 'message system-message';
messageEl.textContent = text;
messagesDiv.appendChild(messageEl);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
// Escape HTML
function escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Send message
function sendMessage(): void {
const text = messageInput.value.trim();
if (!text) return;
const member = channel.members.me;
const message: Message = {
user: member.info.name,
text: text,
timestamp: new Date().toISOString(),
};
channel.trigger('client-message', message);
messageInput.value = '';
}
// Event listeners
sendButton.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e: KeyboardEvent) => {
if (e.key === 'Enter') {
sendMessage();
}
});Create a presence.js file:
// Simulate user ID (in production, get from authentication)
const currentUserId = `user-${Math.floor(Math.random() * 3) + 1}`;
// Initialize Pusher client
const pusher = new Pusher('app-key', {
wsHost: 'localhost',
wsPort: 6001,
forceTLS: false,
enabledTransports: ['ws', 'wss'],
cluster: 'mt1',
disableStats: true,
authEndpoint: `http://localhost:3000/pusher/auth?user_id=${currentUserId}`,
});
// Subscribe to presence channel
const channel = pusher.subscribe('presence-chat-room');
// DOM elements
const messagesDiv = document.getElementById('messages');
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
const statusDiv = document.getElementById('status');
const usersListDiv = document.getElementById('users-list');
const memberCountSpan = document.getElementById('member-count');
// Connection state handlers
pusher.connection.bind('connected', () => {
console.log('Connected to soketi.rs');
statusDiv.textContent = 'Connected';
statusDiv.className = 'connected';
});
pusher.connection.bind('disconnected', () => {
console.log('Disconnected from soketi.rs');
statusDiv.textContent = 'Disconnected';
statusDiv.className = 'disconnected';
});
// Presence channel events
channel.bind('pusher:subscription_succeeded', (members) => {
console.log('Successfully subscribed to presence channel');
updateUsersList(members);
displaySystemMessage(`You joined the room as ${members.me.info.name}`);
});
channel.bind('pusher:member_added', (member) => {
console.log('Member added:', member);
addUserToList(member);
displaySystemMessage(`${member.info.name} joined the room`);
});
channel.bind('pusher:member_removed', (member) => {
console.log('Member removed:', member);
removeUserFromList(member.id);
displaySystemMessage(`${member.info.name} left the room`);
});
// Chat message event
channel.bind('client-message', (data) => {
displayMessage(data);
});
// Update users list
function updateUsersList(members) {
usersListDiv.innerHTML = '';
memberCountSpan.textContent = members.count;
members.each((member) => {
addUserToList(member);
});
}
// Add user to list
function addUserToList(member) {
const existingUser = document.getElementById(`user-${member.id}`);
if (existingUser) return;
const userEl = document.createElement('li');
userEl.id = `user-${member.id}`;
userEl.className = 'user-item';
userEl.innerHTML = `
<div class="user-status"></div>
<div class="user-info">
<div class="user-name">${member.info.name}</div>
<div class="user-email">${member.info.email}</div>
</div>
`;
usersListDiv.appendChild(userEl);
updateMemberCount();
}
// Remove user from list
function removeUserFromList(userId) {
const userEl = document.getElementById(`user-${userId}`);
if (userEl) {
userEl.remove();
updateMemberCount();
}
}
// Update member count
function updateMemberCount() {
const count = usersListDiv.children.length;
memberCountSpan.textContent = count;
}
// Display chat message
function displayMessage(data) {
const messageEl = document.createElement('div');
messageEl.className = 'message';
const time = new Date(data.timestamp).toLocaleTimeString();
messageEl.innerHTML = `
<span class="message-user">${data.user}:</span>
<span class="message-text">${escapeHtml(data.text)}</span>
<span class="message-time">${time}</span>
`;
messagesDiv.appendChild(messageEl);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
// Display system message
function displaySystemMessage(text) {
const messageEl = document.createElement('div');
messageEl.className = 'message system-message';
messageEl.textContent = text;
messagesDiv.appendChild(messageEl);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
// Escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Send message
function sendMessage() {
const text = messageInput.value.trim();
if (!text) return;
const member = channel.members.me;
const message = {
user: member.info.name,
text: text,
timestamp: new Date().toISOString(),
};
channel.trigger('client-message', message);
messageInput.value = '';
}
// Event listeners
sendButton.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage();
}
});Auth server running on http://localhost:3000
Connected to soketi.rs
Successfully subscribed to presence channel
Member added: { id: 'user-2', info: { name: 'Bob Smith', email: 'bob@example.com' } }
Status Bar:
Connected
Online Users Panel:
Online Users
2 users online
● Alice Johnson
alice@example.com
● Bob Smith
bob@example.com
Chat Messages:
You joined the room as Alice Johnson
Bob Smith joined the room
Alice Johnson: Hello everyone!
Bob Smith: Hi Alice!
Charlie Brown joined the room
Charlie Brown: Hey folks!
Bob Smith left the room
let typingTimeout: NodeJS.Timeout;
messageInput.addEventListener('input', () => {
channel.trigger('client-typing', {
user: channel.members.me.info.name,
});
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
channel.trigger('client-stopped-typing', {
user: channel.members.me.info.name,
});
}, 1000);
});
channel.bind('client-typing', (data: { user: string }) => {
// Show typing indicator
console.log(`${data.user} is typing...`);
});// Update user status
function updateUserStatus(status: 'online' | 'away' | 'busy') {
channel.trigger('client-status-change', {
userId: channel.members.me.id,
status: status,
});
}// Send direct message to specific user
function sendDirectMessage(recipientId: string, message: string) {
channel.trigger('client-direct-message', {
from: channel.members.me.id,
to: recipientId,
message: message,
});
}