Episode 3 — NodeJS MongoDB Backend Architecture / 3.15 — Realtime Communication WebSockets
3.15.d — Rooms and Namespaces
Rooms and namespaces are Socket.io's built-in mechanisms for organizing connections. Rooms group sockets for targeted broadcasting within a namespace, while namespaces create completely separate communication channels on the same underlying connection.
<< Previous: 3.15.c — Socket.io Setup & Basics | Next: 3.15.e — Socket.io Middleware >>
1. What Are Rooms?
Rooms are arbitrary channels that sockets can join and leave. They allow you to broadcast events to a subset of connected clients instead of all of them.
┌─────────────────────────────────────────┐
│ Socket.io Server │
│ │
│ Room: "general" Room: "sports" │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ A │ │ B │ │ B │ │ C │ │
│ └────┘ └────┘ └────┘ └────┘ │
│ │
│ Room: "tech" │
│ ┌────┐ ┌────┐ ┌────┐ │
│ │ A │ │ C │ │ D │ │
│ └────┘ └────┘ └────┘ │
└─────────────────────────────────────────┘
Note: Socket B is in both "general" and "sports" rooms.
A socket can be in multiple rooms simultaneously.
Key facts about rooms:
- Rooms are server-side only -- the client does not know which rooms it is in
- A socket can be in multiple rooms at the same time
- Each socket automatically joins a room with its own
socket.id - Rooms are created automatically when a socket joins; destroyed when empty
2. Joining and Leaving Rooms
io.on('connection', (socket) => {
// JOIN a room
socket.on('join-room', (roomName) => {
socket.join(roomName);
console.log(`${socket.id} joined room: ${roomName}`);
// Notify others in the room
socket.to(roomName).emit('user-joined', {
userId: socket.id,
room: roomName,
message: `A new user joined ${roomName}`
});
});
// LEAVE a room
socket.on('leave-room', (roomName) => {
socket.leave(roomName);
console.log(`${socket.id} left room: ${roomName}`);
socket.to(roomName).emit('user-left', {
userId: socket.id,
room: roomName
});
});
// On disconnect, socket automatically leaves all rooms
socket.on('disconnect', () => {
console.log(`${socket.id} disconnected and left all rooms`);
});
});
3. Emitting to Rooms
io.on('connection', (socket) => {
socket.on('room-message', (data) => {
const { room, text } = data;
// Send to ALL sockets in the room (including sender)
io.to(room).emit('new-message', {
user: socket.id,
text,
room,
timestamp: Date.now()
});
// Send to ALL sockets in the room EXCEPT sender
socket.to(room).emit('new-message', {
user: socket.id,
text,
room,
timestamp: Date.now()
});
// Send to MULTIPLE rooms at once
io.to('room1').to('room2').emit('announcement', {
text: 'This goes to both rooms'
});
// Send to a room EXCEPT specific sockets
io.to(room).except(someSocketId).emit('event', data);
});
});
4. Getting Room Information
io.on('connection', (socket) => {
// Get all sockets in a room
socket.on('get-room-members', async (roomName) => {
const sockets = await io.in(roomName).fetchSockets();
const members = sockets.map(s => ({
id: s.id,
username: s.data.username // if you stored data on socket
}));
socket.emit('room-members', { room: roomName, members });
});
// Get the number of sockets in a room
socket.on('get-room-size', async (roomName) => {
const sockets = await io.in(roomName).fetchSockets();
socket.emit('room-size', {
room: roomName,
count: sockets.length
});
});
// Get all rooms that a socket is in
socket.on('my-rooms', () => {
// socket.rooms is a Set containing at least socket.id
const rooms = Array.from(socket.rooms).filter(r => r !== socket.id);
socket.emit('your-rooms', rooms);
});
// Get all existing rooms on the server
socket.on('list-all-rooms', () => {
const rooms = io.sockets.adapter.rooms;
const roomList = [];
rooms.forEach((sockets, roomName) => {
// Filter out personal rooms (rooms named after socket IDs)
if (!io.sockets.sockets.has(roomName)) {
roomList.push({ name: roomName, size: sockets.size });
}
});
socket.emit('all-rooms', roomList);
});
});
5. Common Room Use Cases
Chat Rooms
io.on('connection', (socket) => {
socket.on('join-chat-room', ({ roomId, username }) => {
socket.data.username = username;
socket.join(`chat:${roomId}`);
io.to(`chat:${roomId}`).emit('system', {
text: `${username} joined the room`
});
});
socket.on('send-message', ({ roomId, text }) => {
io.to(`chat:${roomId}`).emit('message', {
user: socket.data.username,
text,
timestamp: Date.now()
});
});
});
User-Specific Rooms (Private Notifications)
io.on('connection', (socket) => {
// Each user joins a personal room based on their userId
const userId = socket.handshake.auth.userId;
socket.join(`user:${userId}`);
// Now you can send to a specific user from ANYWHERE in your app
});
// In any route handler or service:
function sendNotification(userId, notification) {
io.to(`user:${userId}`).emit('notification', notification);
}
// Example: when someone likes a post
app.post('/api/posts/:id/like', async (req, res) => {
const post = await Post.findById(req.params.id);
// ... save the like ...
// Send real-time notification to post author
sendNotification(post.authorId, {
type: 'like',
message: `${req.user.name} liked your post`,
postId: post._id
});
res.json({ success: true });
});
Game Lobbies
const games = new Map();
io.on('connection', (socket) => {
socket.on('create-game', (gameName) => {
const gameId = `game:${Date.now()}`;
games.set(gameId, {
name: gameName,
host: socket.id,
players: [socket.id],
status: 'waiting'
});
socket.join(gameId);
socket.emit('game-created', { gameId });
io.emit('games-updated', Array.from(games.entries()));
});
socket.on('join-game', (gameId) => {
const game = games.get(gameId);
if (!game || game.players.length >= 4) {
return socket.emit('error', 'Cannot join this game');
}
game.players.push(socket.id);
socket.join(gameId);
io.to(gameId).emit('player-joined', {
playerId: socket.id,
playerCount: game.players.length
});
});
socket.on('game-action', ({ gameId, action }) => {
// Broadcast game action to all players in the game
socket.to(gameId).emit('game-update', {
playerId: socket.id,
action
});
});
});
6. What Are Namespaces?
Namespaces are separate communication channels that share the same underlying connection. They are identified by a path (e.g., /admin, /chat, /notifications).
┌───────────────────────────────────────────────┐
│ Single Connection │
│ │
│ Namespace: / Namespace: /admin │
│ (default) │
│ ┌────┐ ┌────┐ ┌────┐ │
│ │All │ │Usr │ │Adm │ │
│ │user│ │conn│ │only│ │
│ └────┘ └────┘ └────┘ │
│ │
│ Namespace: /chat Namespace: /dashboard │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │Usr1│ │Usr2│ │Mngr│ │Exec│ │
│ └────┘ └────┘ └────┘ └────┘ │
└───────────────────────────────────────────────┘
7. Creating and Using Namespaces
Server-Side
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = new Server(server);
// DEFAULT namespace (/)
io.on('connection', (socket) => {
console.log('[main] connected:', socket.id);
socket.emit('welcome', 'Connected to main namespace');
});
// ADMIN namespace
const adminNamespace = io.of('/admin');
adminNamespace.on('connection', (socket) => {
console.log('[admin] connected:', socket.id);
socket.emit('welcome', 'Connected to admin namespace');
socket.on('server-command', (command) => {
console.log('Admin command:', command);
// Only admin sockets receive this
adminNamespace.emit('command-executed', { command, status: 'ok' });
});
});
// CHAT namespace
const chatNamespace = io.of('/chat');
chatNamespace.on('connection', (socket) => {
console.log('[chat] connected:', socket.id);
socket.on('message', (data) => {
chatNamespace.emit('message', data); // Only goes to /chat clients
});
});
// NOTIFICATIONS namespace
const notifNamespace = io.of('/notifications');
notifNamespace.on('connection', (socket) => {
const userId = socket.handshake.auth.userId;
socket.join(`user:${userId}`);
console.log(`[notifications] User ${userId} connected`);
});
// Send notification from anywhere in the app
function pushNotification(userId, data) {
notifNamespace.to(`user:${userId}`).emit('new-notification', data);
}
server.listen(3000);
Client-Side
// Connect to DEFAULT namespace
const mainSocket = io('http://localhost:3000');
// Connect to ADMIN namespace
const adminSocket = io('http://localhost:3000/admin');
// Connect to CHAT namespace
const chatSocket = io('http://localhost:3000/chat');
// Connect to NOTIFICATIONS namespace
const notifSocket = io('http://localhost:3000/notifications', {
auth: { userId: 'user123' }
});
// Each namespace connection is independent
chatSocket.emit('message', { text: 'Hello chat!' });
adminSocket.emit('server-command', 'restart-cache');
notifSocket.on('new-notification', (data) => {
showNotificationBanner(data);
});
8. Namespace vs Room Comparison
| Feature | Namespace | Room |
|---|---|---|
| Created by | Server code (io.of()) | Any socket joining (socket.join()) |
| Client awareness | Client connects explicitly | Server-side only, client unaware |
| Separate connection | Multiplexed on same connection | Within a namespace |
| Middleware | Yes, per-namespace | No, uses namespace middleware |
| Use case | Separating app features | Grouping users within a feature |
| Path | URL path (/admin, /chat) | Arbitrary string name |
| Hierarchy | Top-level separation | Within a namespace |
Think of it this way:
- Namespaces = separate apps on the same server (chat module, notification module, admin module)
- Rooms = groups within an app (chat rooms, game lobbies, user channels)
Namespace: /chat
├── Room: "general" → users in general chat
├── Room: "sports" → users in sports chat
└── Room: "tech" → users in tech chat
Namespace: /games
├── Room: "game-1" → players in game 1
└── Room: "game-2" → players in game 2
9. Dynamic Rooms Pattern
// Dynamic project-based rooms
io.on('connection', (socket) => {
const userId = socket.handshake.auth.userId;
socket.on('open-project', async (projectId) => {
// Verify user has access to this project
const hasAccess = await checkProjectAccess(userId, projectId);
if (!hasAccess) {
return socket.emit('error', { message: 'Access denied' });
}
// Leave any previously opened project room
const currentRooms = Array.from(socket.rooms);
currentRooms.forEach(room => {
if (room.startsWith('project:') && room !== `project:${projectId}`) {
socket.leave(room);
}
});
// Join the new project room
socket.join(`project:${projectId}`);
// Notify team members
socket.to(`project:${projectId}`).emit('team-member-online', {
userId,
projectId
});
// Send current project state to the joining user
const activeUsers = await io.in(`project:${projectId}`).fetchSockets();
socket.emit('project-state', {
activeMembers: activeUsers.length,
projectId
});
});
socket.on('project-update', ({ projectId, changes }) => {
// Broadcast changes to all project collaborators except sender
socket.to(`project:${projectId}`).emit('project-changed', {
userId,
changes,
timestamp: Date.now()
});
});
});
10. Multi-Room Chat Application
// Complete multi-room chat with room management
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = new Server(server, { cors: { origin: '*' } });
// Track rooms and their metadata
const chatRooms = new Map();
// Initialize default rooms
['general', 'random', 'tech', 'gaming'].forEach(name => {
chatRooms.set(name, {
name,
description: `${name} chat room`,
createdAt: new Date(),
messageHistory: []
});
});
io.on('connection', (socket) => {
let currentUser = null;
// User authentication / registration
socket.on('register', (username) => {
currentUser = username;
socket.data.username = username;
// Send available rooms list
const roomList = Array.from(chatRooms.entries()).map(([name, data]) => ({
name,
description: data.description,
memberCount: io.sockets.adapter.rooms.get(name)?.size || 0
}));
socket.emit('room-list', roomList);
});
// Join a room
socket.on('join-room', (roomName) => {
if (!chatRooms.has(roomName)) {
return socket.emit('error', { message: `Room "${roomName}" does not exist` });
}
socket.join(roomName);
// Send recent message history
const room = chatRooms.get(roomName);
const recentMessages = room.messageHistory.slice(-50);
socket.emit('message-history', { room: roomName, messages: recentMessages });
// Notify room
io.to(roomName).emit('system-message', {
room: roomName,
text: `${currentUser} joined the room`,
timestamp: Date.now()
});
// Update member count for everyone
broadcastRoomUpdate(roomName);
});
// Leave a room
socket.on('leave-room', (roomName) => {
socket.leave(roomName);
io.to(roomName).emit('system-message', {
room: roomName,
text: `${currentUser} left the room`,
timestamp: Date.now()
});
broadcastRoomUpdate(roomName);
});
// Send message to a room
socket.on('send-message', ({ room, text }) => {
if (!currentUser || !chatRooms.has(room)) return;
const message = {
id: Date.now(),
user: currentUser,
text,
room,
timestamp: Date.now()
};
// Store in history
chatRooms.get(room).messageHistory.push(message);
// Keep only last 200 messages per room
if (chatRooms.get(room).messageHistory.length > 200) {
chatRooms.get(room).messageHistory.shift();
}
io.to(room).emit('new-message', message);
});
// Create a new room
socket.on('create-room', ({ name, description }) => {
if (chatRooms.has(name)) {
return socket.emit('error', { message: 'Room already exists' });
}
chatRooms.set(name, {
name,
description: description || `${name} chat room`,
createdAt: new Date(),
createdBy: currentUser,
messageHistory: []
});
// Notify all connected clients about the new room
io.emit('room-created', { name, description });
});
socket.on('disconnect', () => {
if (currentUser) {
// Notify all rooms this user was in
socket.rooms.forEach(room => {
if (room !== socket.id) {
io.to(room).emit('system-message', {
room,
text: `${currentUser} disconnected`,
timestamp: Date.now()
});
broadcastRoomUpdate(room);
}
});
}
});
});
function broadcastRoomUpdate(roomName) {
const size = io.sockets.adapter.rooms.get(roomName)?.size || 0;
io.emit('room-updated', { name: roomName, memberCount: size });
}
server.listen(3000, () => {
console.log('Multi-room chat running on http://localhost:3000');
});
Key Takeaways
- Rooms group sockets for targeted broadcasting -- server-side only, clients are unaware
- Sockets can be in multiple rooms simultaneously and auto-leave on disconnect
- Use
io.to('room').emit()for all in room,socket.to('room').emit()to exclude sender - Namespaces create separate communication channels on the same connection
- Namespaces separate features (chat, admin, notifications); rooms separate groups within features
- Dynamic rooms enable patterns like user-specific channels, project collaboration, and game lobbies
- Use
fetchSockets()to get room members andio.sockets.adapter.roomsfor room metadata
Explain-It Challenge
Scenario: You are building a Slack-like application with the following requirements:
- Multiple workspaces (each company has its own workspace)
- Channels within each workspace (#general, #engineering, #random)
- Direct messages between users
- A separate admin panel for workspace owners
Design the room and namespace architecture. Decide what should be a namespace vs. a room. Draw out the hierarchy and explain how a message sent in #engineering of "Acme Corp" workspace reaches only the right users.
<< Previous: 3.15.c — Socket.io Setup & Basics | Next: 3.15.e — Socket.io Middleware >>