typescript 164 lines · 4 tabs

Room-Based Live Notifications over a NestJS WebSocket Gateway

Shared by codesnips Aug 2026
4 tabs
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  OnGatewayConnection,
  OnGatewayDisconnect,
  MessageBody,
  ConnectedSocket,
} from '@nestjs/websockets';
import { UseGuards, Logger } from '@nestjs/common';
import { Server, Socket } from 'socket.io';
import { AuthService } from '../auth/auth.service';
import { RoomSubscriptionService } from './room-subscription.service';
import { WsAuthGuard } from './ws-auth.guard';

@WebSocketGateway({ namespace: '/notifications', cors: { origin: true } })
export class NotificationsGateway
  implements OnGatewayConnection, OnGatewayDisconnect
{
  @WebSocketServer()
  server: Server;

  private readonly logger = new Logger(NotificationsGateway.name);

  constructor(
    private readonly auth: AuthService,
    private readonly rooms: RoomSubscriptionService,
  ) {}

  async handleConnection(socket: Socket): Promise<void> {
    const token = socket.handshake.auth?.token ?? socket.handshake.query?.token;
    const user = await this.auth.verifyToken(String(token ?? ''));

    if (!user) {
      socket.emit('error', { message: 'unauthorized' });
      socket.disconnect(true);
      return;
    }

    socket.data.user = user;
    await this.rooms.subscribeToDefaults(socket, user);
    this.logger.log(`socket ${socket.id} connected as user ${user.id}`);
  }

  handleDisconnect(socket: Socket): void {
    this.logger.log(`socket ${socket.id} disconnected`);
  }

  @UseGuards(WsAuthGuard)
  @SubscribeMessage('subscribe:topic')
  async subscribeTopic(
    @ConnectedSocket() socket: Socket,
    @MessageBody() body: { topic: string },
  ): Promise<{ subscribed: string }> {
    await this.rooms.join(socket, socket.data.user, `topic:${body.topic}`);
    return { subscribed: body.topic };
  }
}
4 files · typescript Explain with highlit

This snippet shows how a NestJS application delivers per-user live notifications over a Socket.IO gateway using a room-based subscription model. The central idea is that instead of tracking individual socket ids, each authenticated connection joins one or more logical rooms (like user:42 or org:7), and broadcasts target those room names. Rooms give a natural fan-out primitive: a single emit reaches every device a user has open, and Socket.IO handles the membership bookkeeping.

In notifications.gateway.ts, the @WebSocketGateway decorator configures the namespace and CORS, and handleConnection runs on every new socket. It resolves the user from the handshake token via AuthService, disconnects unauthenticated clients, and then delegates to the subscription service to place the socket into its rooms. The @SubscribeMessage('subscribe:topic') handler lets clients opt into additional topic rooms at runtime, but only after the guard authorizes them, which prevents a client from listening to arbitrary rooms.

In room-subscription.service.ts, room naming is centralized so producers and consumers agree on the same keys. subscribeToDefaults joins the personal and org rooms, while join/leave wrap socket.join with validation so a client cannot subscribe to a room it has no claim to. Keeping this logic in a service rather than the gateway makes the rules testable and reusable from HTTP controllers.

In notifications.service.ts, broadcastToUser and broadcastToTopic are the public API other modules call. They resolve the room name and use server.to(room).emit(...), so the notification producer never needs a socket reference. This decouples the domain code that creates a notification from the transport that delivers it.

The WsAuthGuard in ws-auth.guard.ts re-validates the token on message events, because the handshake identity can be stale by the time a later message arrives. A key trade-off is that Socket.IO rooms live in a single process; scaling horizontally requires the @socket.io/redis-adapter so emit reaches sockets on other nodes. Edge cases worth noting are duplicate connections from multiple tabs (handled naturally by rooms), and cleaning up on handleDisconnect, which Socket.IO does automatically for room membership.


Related snips

ruby
payload = {
  sub: user.id,
  iss: 'https://auth.example.com',
  aud: 'codesnips-api',
  exp: 15.minutes.from_now.to_i,
  iat: Time.now.to_i,

JWT issuance and verification without common footguns

jwt authentication api
by Kai Nakamura 2 tabs
ruby
json.array! @posts do |post|
  json.cache! ['v1', post], expires_in: 1.hour do
    json.id post.id
    json.title post.title
    json.excerpt post.excerpt
    json.published_at post.published_at

Fragment caching for expensive JSON serialization

rails caching performance
by Alex Kumar 1 tab
typescript
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";

const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";

JWT access + refresh token rotation (conceptual)

security node jwt
by codesnips 3 tabs
ruby
class Comment < ApplicationRecord
  belongs_to :article
  belongs_to :author, class_name: "User"

  validates :body, presence: true, length: { maximum: 2_000 }

Live comments with model broadcasts + turbo_stream_from

rails hotwire turbo
by codesnips 4 tabs
erb
<%# private stream: turbo signs the serialized record name %>
<%= turbo_stream_from current_user %>

<section class="notifications">
  <h1>Notifications</h1>

Turbo Streams + authorization: signed per-user stream name

rails turbo hotwire
by codesnips 3 tabs
ruby
raw_token = SecureRandom.urlsafe_base64(32)
token_digest = Digest::SHA256.hexdigest(raw_token)

PasswordReset.create!(
  user: user,
  token_digest: token_digest,

Secure random token generation for sessions and recovery flows

randomness tokens authentication
by Kai Nakamura 1 tab

Share this code

Here's the card — post it anywhere.

Room-Based Live Notifications over a NestJS WebSocket Gateway — share card
Link copied