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

FeatureNamespaceRoom
Created byServer code (io.of())Any socket joining (socket.join())
Client awarenessClient connects explicitlyServer-side only, client unaware
Separate connectionMultiplexed on same connectionWithin a namespace
MiddlewareYes, per-namespaceNo, uses namespace middleware
Use caseSeparating app featuresGrouping users within a feature
PathURL path (/admin, /chat)Arbitrary string name
HierarchyTop-level separationWithin 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

  1. Rooms group sockets for targeted broadcasting -- server-side only, clients are unaware
  2. Sockets can be in multiple rooms simultaneously and auto-leave on disconnect
  3. Use io.to('room').emit() for all in room, socket.to('room').emit() to exclude sender
  4. Namespaces create separate communication channels on the same connection
  5. Namespaces separate features (chat, admin, notifications); rooms separate groups within features
  6. Dynamic rooms enable patterns like user-specific channels, project collaboration, and game lobbies
  7. Use fetchSockets() to get room members and io.sockets.adapter.rooms for 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 >>