Md Nazmus Shakib
Back to articles
Technology

Architecting Real-Time Notification Systems for SaaS using WebSockets and Socket.io

Md Nazmus Shakib

Md Nazmus Shakib

MERN Stack Developer

45m Total Read
Architecting Real-Time Notification Systems for SaaS using WebSockets and Socket.io

In modern SaaS applications—whether it is a live food delivery tracker or a dental clinic appointment dashboard—expecting users to manually refresh their browsers to see new data is an obsolete paradigm. While traditional HTTP polling (making REST API calls every 5 seconds) can simulate real-time updates, it overwhelms the server with thousands of redundant requests and drains mobile battery life.

The architectural solution is establishing a persistent, bi-directional communication channel using WebSockets. In this technical guide, we will architect an enterprise-grade, real-time notification system using Node.js, Socket.io, and Next.js, while addressing critical challenges like JWT authentication during handshakes and horizontal scaling via Redis.

1. Securing the WebSocket Handshake

A common vulnerability in real-time applications is leaving the WebSocket endpoint open to the public. Because WebSockets initiate via an HTTP Upgrade request, you cannot rely on standard API middleware. You must intercept the Socket.io handshake and verify the user's JSON Web Token (JWT) before allowing the TCP connection to persist.

Once authenticated, the user should be immediately assigned to a specific "Room" (e.g., their tenantId or userId) to ensure data isolation.

server/socket.ts
import { Server } from "socket.io";
import jwt from "jsonwebtoken";

const io = new Server(server, {
  cors: { origin: "https://your-saas-platform.com", methods: ["GET", "POST"] }
});

// Middleware: JWT Authentication on Handshake
io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  if (!token) return next(new Error("Authentication error: No token provided"));

  jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
    if (err) return next(new Error("Authentication error: Invalid token"));
    
    // Attach user details to the socket instance
    socket.data.user = decoded;
    next();
  });
});

// Connection Handler
io.on("connection", (socket) => {
  const { tenantId, userId, role } = socket.data.user;

  // Isolate users by Tenant and User ID Rooms
  socket.join(`tenant_${tenantId}`);
  socket.join(`user_${userId}`);

  // Admin specific room
  if (role === "ADMIN") socket.join(`admin_${tenantId}`);

  socket.on("disconnect", () => {
    console.log(`User ${userId} disconnected.`);
  });
});
Architectural Insight: Rooms vs. Broadcasting

Never use io.emit() in a multi-tenant SaaS application. It broadcasts the event to every single connected client, leading to severe data leaks. Always target specific rooms using io.to('room_name').emit() to ensure multi-tenant data isolation.

2. Client-Side Integration in Next.js (Custom Hooks)

In a React/Next.js environment, managing the WebSocket lifecycle is tricky. If you initialize the connection directly inside a component without proper cleanup, React's strict mode (or simple component re-renders) will spawn multiple identical socket connections, resulting in duplicate notifications and memory leaks.

To mitigate this, you must engineer a Singleton Custom Hook that manages the connection state globally.

hooks/useSocket.ts
import { useEffect, useRef } from "react";
import { io, Socket } from "socket.io-client";
import { useSession } from "next-auth/react";
import { toast } from "react-hot-toast";

export const useSocket = () => {
  const { data: session } = useSession();
  const socketRef = useRef<Socket | null>(null);

  useEffect(() => {
    if (!session?.user?.accessToken) return;

    // Initialize connection with Auth Token
    socketRef.current = io("https://api.your-saas.com", {
      auth: { token: session.user.accessToken },
      transports: ["websocket"] // Force WebSocket over long-polling for speed
    });

    // Event Listeners
    socketRef.current.on("new_appointment", (data) => {
      toast.success(`New Appointment Booked: ${data.patientName}`);
      // Dispatch to Redux/Zustand or trigger SWR revalidation here
    });

    // Critical Cleanup Function
    return () => {
      if (socketRef.current) {
        socketRef.current.disconnect();
      }
    };
  }, [session]);

  return socketRef.current;
};

3. The Horizontal Scaling Problem: Redis Adapter

WebSockets are stateful TCP connections. This creates a massive architectural hurdle when scaling your infrastructure. If you load-balance your backend across three Node.js instances (Server A, B, and C), User 1 might connect to Server A, and User 2 to Server B.

If Server A attempts to emit a message to a room containing User 2, the message will drop into the void because Server A knows nothing about the connections held by Server B.

The solution is the Socket.io Redis Adapter. By placing Redis in the middle, it acts as a Pub/Sub message broker. When Server A emits a message, it publishes it to Redis. Redis then instantly pushes that message to all other Node instances, ensuring the event reaches the user regardless of which physical server they are connected to.

import { createAdapter } from "@socket.io/redis-adapter";
import { createClient } from "redis";

const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();

Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
  io.adapter(createAdapter(pubClient, subClient));
  console.log("Redis Adapter Attached. WebSockets are now Horizontally Scalable.");
});

Conclusion

Implementing real-time features elevates an application from a static web page into a dynamic, living software platform. By enforcing JWT security at the handshake layer, meticulously managing React component lifecycles, and utilizing Redis for cross-node communication, you can architect a robust WebSocket ecosystem capable of handling millions of concurrent, real-time events.

#WebSockets#Socket.io#Real-Time Architecture#Next.js#Node.js#SaaS#Redis
Md Nazmus Shakib
Written By

Md Nazmus Shakib

MERN Stack Developer

Specializing in the MERN stack and modern frameworks to engineer secure, SEO-optimized, and scalable digital solutions for visionary brands.

More about the author →