Tech TutorBangla Web Documentation
Portfolio
HomeHTMLCSSTailwind CSSJavaScriptTypeScriptReactNext.jsNode.jsExpressSocket.IOExpoWordPressPostgreSQLPrismaPostgresNestJSMongoDBDockerTanStackTanStack TableRTK QueryGitGitHub
  • Socket.IO Documentation25টি অধ্যায়
প্রযুক্তি নির্বাচন
HomeHTMLCSSTailwind CSSJavaScriptTypeScriptReactNext.jsNode.jsExpressSocket.IOExpoWordPressPostgreSQLPrismaPostgresNestJSMongoDBDockerTanStackTanStack TableRTK QueryGitGitHub
  • Introduction
  • Why Use Socket.IO
  • Socket.IO vs WebSocket
  • Installation
  • Client-Server Setup
  • Connection Lifecycle
  • Events
  • Data Transfer
  • Broadcasting
  • Rooms
  • Namespaces
  • Private & Group Messaging
  • Acknowledgements & Timeouts
  • Connection State Recovery
  • Reconnection & Errors
  • Authentication
  • Middleware
  • CORS Configuration
  • Express Integration
  • Next.js Integration
  • TypeScript
  • Chat Application
  • Best Practices
  • Next.js + Express Chat App
  • Next.js + Express Notifications
  1. Docs
  2. Socket.IO
  3. Build a Real-Time Notification System with Next.js, Express.js, and Socket.IO
Socket.IOAdvanced

Build a Real-Time Notification System with Next.js, Express.js, and Socket.IO

Next.js (TypeScript) frontend, Express (TypeScript) backend আর Socket.IO v4 দিয়ে user-specific real-time notification system — bell, unread count, mark-as-read ও reconnection সহ সম্পূর্ণ Bangla project গাইড।

120টি অংশবাংলা ভাষায়

এখন পর্যন্ত শেখা ধারণাগুলো — rooms, namespaces, authentication middleware, reconnection আর typed event contract — একসাথে কাজে লাগিয়ে একটি real-time notification system বানাই। Frontend হবে Next.js (TypeScript), backend Express (TypeScript), আর real-time delivery-এর জন্য Socket.IO v4।

লক্ষ্য শুধু একটা bell icon নয় — বরং সঠিক architecture: প্রতিটি notification শুধু তার নিজের user-এর কাছে যাবে, identity server-এ যাচাই হবে, আর client-এর পাঠানো user id কখনো বিশ্বাস করা হবে না।

শেখার লক্ষ্য

  • Next.js client-কে Express + Socket.IO server-এর সাথে যুক্ত করা
  • user-specific room দিয়ে private notification সঠিক user-এর কাছে পাঠানো
  • handshake auth middleware দিয়ে identity যাচাই করা
  • notification bell, unread badge আর panel বানানো
  • mark-as-read / mark-all-read implement করা
  • reconnection-এ সংরক্ষিত notification আনা ও state sync রাখা
  • client-এর ডেটা validate করা এবং সব ডেটা নিরাপদে render করা
Documentation example মাত্র

এটি শুধু একটি শেখার example — Tech Tutor ওয়েবসাইটে app হিসেবে যোগ করা হয়নি। এখানে code পড়ে নিজের project-এ ব্যবহার করবেন (তুলনায় দেখুন Real-Time Chat Application ও Next.js + Express Chat App)।

Project Overview

আমাদের notification system-এর বারোটি target:

#Feature
1Next.js client → Express Socket.IO server-এ connect
2Connection status display
3Authenticated identity (verified user)
4Real-time notification পাওয়া
5Title, body, timestamp ও read state display
6Online/unread badge ও live count
7Notification-এ mark-as-read
8সব notification mark-all-read
9Simulated order status update → notification
10Reconnection ও missed notification retrieval
11Validation, authorization ও safe rendering
12Responsive, accessible panel UI

Architecture

দুইটি আলাদা process চলে — Next.js (port 3000) আর Express + Socket.IO (port 4000)। Browser একটি token নিয়ে Socket.IO server-এ connect করে; server token verify করে userId বের করে এবং একটি user-specific room-এ socket-কে যোগ করে। এরপর যেকোনো notification শুধু সেই room-এ emit হয় — কখনো সব client-এ নয়।

request flowtext
text
Browser (Next.js UI)
  useNotifications ──io(auth.token)──►  Express + Socket.IO (port 4000)
        ▲                                     │
        │                          io.use(verifyToken) → user:<id> room
        │                                     │
        │  notification:new  ◄────────────────┤  emitNotification(userId, n)
        │                                     │
        │  notification:fetch (ack: list) ───►│  in-memory store (Map)
        │                                     │
Order service ──POST /api/orders/:id/status──► createNotification() → emit

Event flow:

  1. Client handshake-এ auth.token পাঠায় → server verifyToken দিয়ে userId নেয় ও user:<id> room-এ join করায়
  2. কোনো order status বদলালে server createNotification() করে notification:new কেবল সেই user-এর room-এ পাঠায়
  3. Client notification:mark-read পাঠায় → server ownership যাচাই করে notification:read emit করে
  4. Client notification:fetch পাঠায় → server-এর সংরক্ষিত list ack করে (reconnect-এ missed পাওয়ার জন্য)
  5. Disconnect হলে কিছুই broadcast হয় না — queue/DB না থাকলে delivery হারাতে পারে

Prerequisites

  • Node.js 20+ ও npm
  • TypeScript-এর মৌলিক ধারণা (Socket.IO with TypeScript)
  • React hooks (useState, useEffect, useRef, useCallback) সম্পর্কে ধারণা
  • Rooms, authentication ও middleware সম্পর্কে ধারণা (Rooms, Authentication, Middleware)

Folder Structure

আগের project-এর মতোই একটি ছোট monorepo — root-এ npm workspaces, তিনটি package: shared, server, client। shared শুধু TypeScript type ধরে, যাতে server ও client একই event contract ব্যবহার করে।

folder structuretext
text
socketio-notify-app/
├── package.json            # workspaces: shared, server, client
├── shared/
│   ├── package.json
│   └── src/events.ts       # shared event + data types
├── server/
│   ├── .env
│   ├── package.json
│   ├── tsconfig.json
│   └── src/
│       ├── config.ts
│       ├── auth.ts         # token verify (teaching-only)
│       ├── store.ts        # in-memory notification store
│       ├── socket.ts       # Socket.IO + auth + rooms
│       └── index.ts        # Express routes + http server
└── client/                 # create-next-app দিয়ে তৈরি
    ├── .env.local
    ├── app/
    │   └── page.tsx
    ├── components/
    │   ├── notification-bell.tsx
    │   └── order-simulator.tsx
    └── hooks/
        └── use-notifications.ts

Step 1 — Project Init

Root-এ workspace বানান, তারপর তিনটি package install করুন।

terminalbash
bash
mkdir socketio-notify-app
cd socketio-notify-app

# shared workspace
mkdir shared/src

# server workspace
mkdir server/src

# Next.js client
npx create-next-app@latest client --ts --tailwind --eslint --app --no-src-dir --import-alias "@/*" --use-npm

# root-এ সব install (workspaces)
npm install
package.jsonjson
json
{
  "name": "socketio-notify-app",
  "private": true,
  "workspaces": ["shared", "server", "client"]
}

তারপর server ও client-এর dependency যোগ করুন। @notify/shared type-only, তাই এতে runtime dependency লাগে না।

install dependenciesbash
bash
# Express + Socket.IO server
npm install -w server express socket.io cors dotenv
npm install -w server -D typescript tsx @types/express @types/cors @types/node

# Next.js-এ Socket.IO client
npm install -w client socket.io-client
shared/package.jsonjson
json
{
  "name": "@notify/shared",
  "version": "1.0.0",
  "private": true,
  "types": "./src/events.ts",
  "main": "./src/events.ts"
}

Step 2 — Shared Event Contract

shared/src/events.ts

সবচেয়ে গুরুত্বপূর্ণ ভিত্তি হলো event ও type-এর single source of truth। server ও client একই events.ts import करায় event নাম বা payload কখনো mismatch হবে না।

shared/src/events.tstypescript
typescript
// shared/src/events.ts
export interface AppNotification {
  id: string;
  userId: string;
  title: string;
  body: string;
  read: boolean;
  createdAt: number;
}

export interface AckResult {
  ok: boolean;
  error?: string;
}

/** server থেকে client-এ যাওয়া event */
export interface ServerToClientEvents {
  "notification:new": (notification: AppNotification) => void;
  "notification:read": (id: string) => void;
  "notification:all-read": () => void;
}

/** client থেকে server-এ যাওয়া event */
export interface ClientToServerEvents {
  "notification:fetch": (
    since: number,
    ack: (list: AppNotification[]) => void,
  ) => void;
  "notification:mark-read": (
    id: string,
    ack: (result: AckResult) => void,
  ) => void;
  "notification:mark-all-read": (ack: (result: AckResult) => void) => void;
}

/** প্রতিটি socket-এ রাখা server-side verified data */
export interface SocketData {
  userId?: string;
  username?: string;
}

export type InterServerEvents = Record<string, never>;

server/tsconfig.json

server/tsconfig.jsonjson
json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "noEmit": true,
    "types": ["node"]
  },
  "include": ["src"]
}

server/package.json

server/package.jsonjson
json
{
  "name": "server",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "start": "tsx src/index.ts",
    "typecheck": "tsc --noEmit"
  }
}

Step 3 — Express Backend

Express app আর node:http server তৈরি করি। Socket.IO ঠিক এই একই HTTP server-এর উপরে বসবে, তাই একই port-এ REST + realtime দুটোই চলবে (Socket.IO with Express.js)।

server/.env

server/.envbash
bash
# server/.env
PORT=4000
CLIENT_ORIGIN=http://localhost:3000

server/src/config.ts

server/src/config.tstypescript
typescript
// server/src/config.ts
import "dotenv/config";

export const PORT = Number(process.env.PORT ?? 4000);
export const CLIENT_ORIGIN =
  process.env.CLIENT_ORIGIN ?? "http://localhost:3000";

Step 4 — Auth Middleware (identity যাচাই)

এই project-এর মূল নিরাপত্তা এখানেই। client কখনো userId পাঠাবে না — server handshake-এ token verify করে নিজে userId বের করবে। নিচের verifier শুধু শেখানোর জন্য (কোনো আসল secret নেই); production-এ JWT/session verify ব্যবহার করবেন (Authentication)।

server/src/auth.ts

server/src/auth.tstypescript
typescript
// server/src/auth.ts
export interface AuthUser {
  id: string;
  username: string;
}

// ⚠️ teaching-only: token → user map (কোনো আসল secret নেই)।
// production-এ jsonwebtoken/session (httpOnly cookie) দিয়ে verify করুন।
const SESSIONS = new Map<string, AuthUser>([
  ["token-demo-1", { id: "user-1", username: "Ayesha" }],
  ["token-demo-2", { id: "user-2", username: "Rahim" }],
]);

export function verifyToken(token: string): AuthUser | null {
  if (!token) return null;
  return SESSIONS.get(token) ?? null;
}
client-এর userId বিশ্বাস করবেন না

client থেকে পাঠানো কোনো userId বা room নাম কখনো সরাসরি ব্যবহার করবেন না। সবসময় verified token/session থেকে server-এ identity নিন — নাহলে একজন user অন্যের notification পড়তে পারবে।

Step 5 — In-Memory Notification Store

প্রথমে একটি সরল store — Map<userId, AppNotification[]>। এখানে read state-ও থাকে, তাই mark-read server-এ যাচাই করা যায়।

server/src/store.ts

server/src/store.tstypescript
typescript
// server/src/store.ts
import { randomUUID } from "node:crypto";

import type { AppNotification } from "@notify/shared";

// ⚠️ in-memory only: server restart/crash-এ সব notification হারিয়ে যায়।
// production-এ এগুলো database-এ রাখতে হবে (নিচে দেখুন)।
const byUser = new Map<string, AppNotification[]>();

export function listForUser(userId: string): AppNotification[] {
  return byUser.get(userId) ?? [];
}

export function createNotification(input: {
  userId: string;
  title: string;
  body: string;
}): AppNotification {
  const notification: AppNotification = {
    id: crypto.randomUUID(),
    userId: input.userId,
    title: String(input.title).trim().slice(0, 120),
    body: String(input.body).trim().slice(0, 500),
    read: false,
    createdAt: Date.now(),
  };

  const list = byUser.get(notification.userId) ?? [];
  list.unshift(notification);
  if (list.length > 100) list.length = 100; // memory সীমিত রাখুন
  byUser.set(notification.userId, list);

  return notification;
}

/** read flag আপডেট করে; সফল হলে true ফেরায়। */
export function markRead(userId: string, id: string): boolean {
  const found = (byUser.get(userId) ?? []).find((n) => n.id === id);
  if (!found) return false;
  found.read = true;
  return true;
}

export function markAllRead(userId: string): void {
  for (const n of byUser.get(userId) ?? []) n.read = true;
}

Step 5.1 — CORS (explicit origin)

Socket.IO v3 থেকে CORS ডিফল্টভাবে বন্ধ। client আর server আলাদা origin-এ (:3000 বনাম :4000) চলায় নির্দিষ্ট origin দিতে হবে — "*" নয়। বিস্তারিত CORS Configuration-এ।

Step 6 — Socket.IO Server, Auth ও User-Specific Rooms

এখন মূল server logic। io.use() middleware-এ token verify হয়; সফল হলে socket-কে শুধু তার নিজের user:<id> room-এ join করানো হয়। room নাম কখনো client থেকে নেওয়া হয় না — verified userId থেকে server নিজেই বানায়।

server/src/socket.ts

server/src/socket.tstypescript
typescript
// server/src/socket.ts
import type http from "node:http";
import { Server } from "socket.io";

import type {
  AppNotification,
  ClientToServerEvents,
  InterServerEvents,
  ServerToClientEvents,
  SocketData,
} from "@notify/shared";

import { CLIENT_ORIGIN } from "./config";
import { verifyToken } from "./auth";
import { listForUser, markAllRead, markRead } from "./store";

type NotifyServer = Server<
  ClientToServerEvents,
  ServerToClientEvents,
  InterServerEvents,
  SocketData
>;

// শুধু server-এর বানানো room নাম — client এটা দিতে পারে না
const userRoom = (userId: string) => `user:${userId}`;

let ioRef: NotifyServer | null = null;

export function createSocketServer(httpServer: http.Server): NotifyServer {
  const io: NotifyServer = new Server(httpServer, {
    cors: { origin: CLIENT_ORIGIN, methods: ["GET", "POST"] },
  });
  ioRef = io;

  // handshake auth — identity server-এ verify হয়, client-এর দাবি নয়
  io.use((socket, next) => {
    const token = socket.handshake.auth?.token;
    const user = verifyToken(String(token ?? ""));
    if (!user) return next(new Error("unauthorized"));

    socket.data.userId = user.id;
    socket.data.username = user.username;
    next();
  });

  io.on("connection", (socket) => {
    const userId = socket.data.userId;
    if (!userId) return; // auth middleware ছাড়া এখানে আসা উচিত নয়

    // verified identity থেকে room-এ join
    socket.join(userRoom(userId));

    // connect/reconnect-এ সংরক্ষিত notification আনুন
    socket.on("notification:fetch", (since, ack) => {
      const list = listForUser(userId).filter((n) => n.createdAt > since);
      ack(list);
    });

    socket.on("notification:mark-read", (id, ack) => {
      const clean = String(id ?? "").slice(0, 64);
      // ownership সর্বদা server-এর verified userId দিয়ে যাচাই
      const ok = markRead(userId, clean);
      if (ok) io.to(userRoom(userId)).emit("notification:read", clean);
      ack(ok ? { ok: true } : { ok: false, error: "not found" });
    });

    socket.on("notification:mark-all-read", (ack) => {
      markAllRead(userId);
      io.to(userRoom(userId)).emit("notification:all-read");
      ack({ ok: true });
    });
  });

  return io;
}

/** নির্দিষ্ট user-এর room-এ notification emit — কখনো io.emit (broadcast) নয়। */
export function emitNotification(
  userId: string,
  notification: AppNotification,
): void {
  ioRef?.to(userRoom(userId)).emit("notification:new", notification);
}

Line-by-line:

  • io.use(...) — handshake-এ একবার identity যাচাই; ব্যর্থ হলে next(new Error(...)), connection-ই হবে না।
  • socket.join(userRoom(userId)) — room নাম server তৈরি করে verified userId থেকে; client এর উপর কোনো নিয়ন্ত্রণ নেই।
  • io.to(userRoom(userId)).emit(...) — private delivery; io.emit(...) করলে সবার কাছে চলে যেত।
  • markRead(userId, id) — read operation-এ userId দিয়ে authorization বাধ্যতামূলক।

Step 7 — Express Routes ও Simulated Order Update

Express-এ health route, একটি authorization-protected notification list, আর একটি সিমুলেটেড order status route — কোনোটিতেই client-এর পাঠানো userId বিশ্বাস করা হয় না; Bearer token থেকেই user বের করা হয়।

server/src/index.ts

server/src/index.tstypescript
typescript
// server/src/index.ts
import http from "node:http";
import express from "express";
import cors from "cors";

import { CLIENT_ORIGIN, PORT } from "./config";
import { verifyToken } from "./auth";
import { createNotification, listForUser } from "./store";
import { createSocketServer, emitNotification } from "./socket";

const app = express();
app.use(cors({ origin: CLIENT_ORIGIN }));
app.use(express.json());

// Bearer token → verified user. client-এর পাঠানো userId কখনো বিশ্বাস করবেন না।
function readUser(req: express.Request) {
  const header = req.header("authorization") ?? "";
  const token = header.startsWith("Bearer ") ? header.slice(7) : "";
  return verifyToken(token);
}

app.get("/health", (_req, res) => {
  res.json({ ok: true });
});

// authorization: শুধু নিজের notification ফেরত দেয়
app.get("/api/notifications", (req, res) => {
  const user = readUser(req);
  if (!user) return res.status(401).json({ error: "unauthorized" });
  res.json(listForUser(user.id));
});

// ⚠️ সিমুলেটেড order update — বাইরের payment/order API লাগে না।
app.post("/api/orders/:orderId/status", (req, res) => {
  const user = readUser(req);
  if (!user) return res.status(401).json({ error: "unauthorized" });

  const status = String(req.body?.status ?? "").trim();
  const allowed = ["confirmed", "shipped", "delivered"];
  if (!allowed.includes(status)) {
    return res.status(400).json({ error: "invalid status" });
  }

  const orderId = String(req.params.orderId).slice(0, 40);
  const notification = createNotification({
    userId: user.id,
    title: "Order status আপডেট",
    body: `আপনার order ${orderId} এখন ${status}।`,
  });

  // প্রাপক কখনো request body থেকে নয় — verified user থেকেই
  emitNotification(user.id, notification);
  res.json({ ok: true, notification });
});

// Express app-কে একটি http.Server-এ মুড়ে দিন — Socket.IO এখানেই বসবে
const httpServer = http.createServer(app);
createSocketServer(httpServer);

httpServer.listen(PORT, () => {
  console.log(`Server চলছে: http://localhost:${PORT}`);
});
app.listen নয়

Socket.IO যোগ করার পর app.listen(PORT) ব্যবহার করবেন না — httpServer.listen(PORT) করুন। নাহলে Socket.IO কখনো bind হবে না (Express Integration)।

Step 8 — Next.js Client Setup

Socket.IO URL ও demo token environment variable থেকে নেওয়া হবে — কোনো production URL hardcode নয়।

client/.env.local

client/.env.localbash
bash
# client/.env.local
NEXT_PUBLIC_SOCKET_URL=http://localhost:4000
NEXT_PUBLIC_DEMO_TOKEN=token-demo-1
Real token কখনো NEXT_PUBLIC-এ নয়

এখানে token-demo-1 একটি বানানো (non-secret) token। প্রকৃত authentication token NEXT_PUBLIC_* variable-এ রাখবেন না — সেটি browser bundle-এ চলে যায়। production-এ প্রশিক্ষণযোগ্য session/token httpOnly cookie বা server-side route থেকে নিন।

Step 9 — useNotifications Hook (state ও lifecycle)

এই hook-ই socket তৈরি, listener যোগ, notification state, unread count ও cleanup করে। socket কেবল useEffect-এর ভিতরে তৈরি হয় — render-এর সময় নয়।

client/hooks/use-notifications.ts

client/hooks/use-notifications.tstypescript
typescript
// client/hooks/use-notifications.ts
"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { io, type Socket } from "socket.io-client";

import type {
  AppNotification,
  ClientToServerEvents,
  ServerToClientEvents,
} from "@notify/shared";

type NotifySocket = Socket<ServerToClientEvents, ClientToServerEvents>;
type Status = "connecting" | "connected" | "disconnected";

const SOCKET_URL =
  process.env.NEXT_PUBLIC_SOCKET_URL ?? "http://localhost:4000";

export function useNotifications(token: string) {
  const socketRef = useRef<NotifySocket | null>(null);
  const [status, setStatus] = useState<Status>("connecting");
  const [items, setItems] = useState<AppNotification[]>([]);

  useEffect(() => {
    if (!token) return;

    // socket কেবল effect-এ — render-এ নয় (duplicate connection এড়াতে)
    const socket: NotifySocket = io(SOCKET_URL, { auth: { token } });
    socketRef.current = socket;

    // id দিয়ে merge — duplicate আসলেও একবারই থাকবে
    const merge = (list: AppNotification[]) => {
      setItems((prev) => {
        const map = new Map(list.map((n) => [n.id, n]));
        for (const n of prev) if (!map.has(n.id)) map.set(n.id, n);
        return [...map.values()].sort((a, b) => b.createdAt - a.createdAt);
      });
    };

    const onConnect = () => {
      setStatus("connected");
      // connect বা reconnect — সংরক্ষিত notification নিয়ে আসুন
      socket.emit("notification:fetch", 0, merge);
    };
    const onDisconnect = () => setStatus("disconnected");
    const onError = () => setStatus("disconnected");
    const onNew = (n: AppNotification) => merge([n]);
    const onRead = (id: string) =>
      setItems((prev) =>
        prev.map((n) => (n.id === id ? { ...n, read: true } : n)),
      );
    const onAllRead = () =>
      setItems((prev) => prev.map((n) => ({ ...n, read: true })));

    socket.on("connect", onConnect);
    socket.on("disconnect", onDisconnect);
    socket.on("connect_error", onError);
    socket.on("notification:new", onNew);
    socket.on("notification:read", onRead);
    socket.on("notification:all-read", onAllRead);

    // cleanup — listener সরান ও connection বন্ধ করুন (StrictMode/routing)
    return () => {
      socket.off("connect", onConnect);
      socket.off("disconnect", onDisconnect);
      socket.off("connect_error", onError);
      socket.off("notification:new", onNew);
      socket.off("notification:read", onRead);
      socket.off("notification:all-read", onAllRead);
      socket.disconnect();
    };
  }, [token]);

  const unread = items.filter((n) => !n.read).length;

  const markRead = useCallback((id: string) => {
    socketRef.current?.emit("notification:mark-read", id, () => {});
  }, []);

  const markAllRead = useCallback(() => {
    socketRef.current?.emit("notification:mark-all-read", () => {});
  }, []);

  return { status, items, unread, markRead, markAllRead };
}

Step 10 — Notification Bell, Unread Badge ও Panel

Bell-এ unread badge দেখানো হয়; panel-এ title, body, timestamp ও read state। সব text {n.title} আকারে render হয় — React নিজেই escape করে, তাই কখনো dangerouslySetInnerHTML ব্যবহার করবেন না। Escape চাপলে ও বাইরে click করলে panel বন্ধ হয় (accessible)।

client/components/notification-bell.tsx

client/components/notification-bell.tsxtypescript
typescript
// client/components/notification-bell.tsx
"use client";

import { useEffect, useRef, useState } from "react";

import { useNotifications } from "@/hooks/use-notifications";

export function NotificationBell({ token }: { token: string }) {
  const { status, items, unread, markRead, markAllRead } =
    useNotifications(token);
  const [open, setOpen] = useState(false);
  const panelRef = useRef<HTMLDivElement | null>(null);

  // Escape চাপলে বন্ধ — cleanup সহ
  useEffect(() => {
    if (!open) return;
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") setOpen(false);
    };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [open]);

  return (
    <div className="relative">
      <button
        type="button"
        aria-label={"Notifications"}
        aria-expanded={open}
        aria-haspopup="dialog"
        onClick={() => setOpen((v) => !v)}
        className="relative rounded-full border p-2"
      >
        <span aria-hidden>bell</span>
        {unread > 0 ? (
          <span
            aria-label={`${unread} unread`}
            className="absolute -right-1 -top-1 rounded-full bg-red-600 px-1.5 text-xs text-white"
          >
            {unread > 99 ? "99+" : unread}
          </span>
        ) : null}
      </button>

      {open ? (
        <div
          ref={panelRef}
          role="dialog"
          aria-label="Notifications"
          className="absolute right-0 mt-2 w-80 max-w-[90vw] rounded-xl border bg-white p-0 shadow-lg"
        >
          <header className="flex items-center justify-between border-b p-3">
            <span className="font-medium">Notifications</span>
            <button
              type="button"
              onClick={markAllRead}
              disabled={items.every((n) => n.read)}
              className="text-sm text-blue-600 disabled:opacity-40"
            >
              Mark all read
            </button>
          </header>

          <ul className="max-h-96 overflow-y-auto">
            {items.length === 0 ? (
              <li className="p-4 text-sm text-gray-500">
                কোনো notification নেই
              </li>
            ) : (
              items.map((n) => (
                <li
                  key={n.id}
                  className={n.read ? "opacity-60" : "bg-blue-50"}
                >
                  <button
                    type="button"
                    onClick={() => markRead(n.id)}
                    disabled={n.read}
                    className="block w-full p-3 text-left"
                  >
                    <span className="block font-medium">{n.title}</span>
                    <span className="block text-sm text-gray-600">
                      {n.body}
                    </span>
                    <time className="block text-xs text-gray-400">
                      {new Date(n.createdAt).toLocaleString()}
                    </time>
                  </button>
                </li>
              ))
            )}
          </ul>

          <footer className="border-t p-2 text-xs text-gray-500">
            {status === "connected" ? "Live" : "Offline"}
          </footer>
        </div>
      ) : null}
    </div>
  );
}

Step 10.1 — Order Simulator (client trigger)

শুধু demo-র convenience: একটা button চাপলে server-এর simulated order route-এ request যায়, server notification তৈরি করে user room-এ emit করে, আর bell badge সাথে সাথে বাড়ে।

client/components/order-simulator.tsx

client/components/order-simulator.tsxtypescript
typescript
// client/components/order-simulator.tsx
"use client";

import { useState } from "react";

const API_URL = process.env.NEXT_PUBLIC_SOCKET_URL ?? "http://localhost:4000";

export function OrderSimulator({ token }: { token: string }) {
  const [busy, setBusy] = useState(false);
  const [message, setMessage] = useState<string | null>(null);

  async function simulate(status: string) {
    setBusy(true);
    setMessage(null);
    try {
      const res = await fetch(`${API_URL}/api/orders/demo-order/status`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${token}`,
        },
        body: JSON.stringify({ status }),
      });
      setMessage(res.ok ? "Order update পাঠানো হয়েছে" : "অনুরোধ ব্যর্থ");
    } catch {
      setMessage("Server-এ পৌঁছানো যায়নি");
    } finally {
      setBusy(false);
    }
  }

  return (
    <div className="mt-6 flex items-center gap-3">
      <button
        type="button"
        onClick={() => simulate("shipped")}
        disabled={busy}
        className="rounded-lg bg-blue-600 px-3 py-2 text-sm text-white disabled:opacity-50"
      >
        Simulate order shipped
      </button>
      {message ? <span className="text-sm">{message}</span> : null}
    </div>
  );
}

Step 11 — Page

client/app/page.tsx

client/app/page.tsxtypescript
typescript
// client/app/page.tsx
import { NotificationBell } from "@/components/notification-bell";
import { OrderSimulator } from "@/components/order-simulator";

const DEMO_TOKEN = process.env.NEXT_PUBLIC_DEMO_TOKEN ?? "token-demo-1";

export default function Home() {
  return (
    <main className="min-h-screen bg-slate-950 p-8 text-slate-100">
      <div className="mx-auto flex max-w-2xl items-center justify-between">
        <h1 className="text-xl font-semibold">Notification Demo</h1>
        <NotificationBell token={DEMO_TOKEN} />
      </div>
      <div className="mx-auto max-w-2xl">
        <OrderSimulator token={DEMO_TOKEN} />
      </div>
    </main>
  );
}

Step 12 — Reconnection ও Missed Notifications

React-এ socket নিয়ে দুটি বড় ভুল এড়াতে হবে:

  • render-এ socket তৈরি — component body-তে io() কল করলে প্রতিটি render-এ নতুন connection খোলে। সমাধান: useEffect (token deps)।
  • cleanup না করা — route বদলালে listener ও connection জমে leak হয়। useEffect-এর return-এ socket.off(...) ও socket.disconnect() দিন।

Socket.IO by default reconnect করে, আর প্রতিবার connect event আবার চলে — তাই onConnect-এর notification:fetch আবার চলে এবং missed notification ফিরে আসে।

client/hooks/use-notifications.ts (reconnect অংশ)

reconnect fetchtypescript
typescript
// connect বা reconnect দুইবারই চলে
const onConnect = () => {
  setStatus("connected");
  socket.emit("notification:fetch", 0, merge); // missed সহ আনুন
};
connection state recoverytypescript
typescript
// server — connection state recovery (optional)
const io = new Server(httpServer, {
  cors: { origin: CLIENT_ORIGIN },
  connectionStateRecovery: {
    maxDisconnectionDuration: 2 * 60 * 1000,
  },
});
Delivery ≠ durable history

Socket.IO শুধু live delivery নিশ্চিত করে — message broker নয়। server restart হলে in-memory list হারায়, তাই reconnect-এ ফেরানো সম্ভব হবে না। স্থায়ী history দরকার হলে notification অবশ্যই database-এ লিখুন (নিচে দেখুন)। বিস্তারিত Connection State Recovery ও Reconnection & Error Handling-এ।

Step 13 — Run and Test

server চালু করুন

run serverbash
bash
# terminal 1 — backend (port 4000)
npm run dev -w server

# output: Server চলছে: http://localhost:4000

client চালু করুন

run clientbash
bash
# terminal 2 — frontend (port 3000)
npm run dev -w client

তারপর browser-এ http://localhost:3000 খুলুন।

Testing the Complete Application

  1. Connect — page খুললে footer-এ Live দেখুন; bell-এ badge 0
  2. Trigger — Simulate order shipped চাপুন; badge 1 হবে, panel-এ নতুন notification আসবে
  3. Read — notification-এ ক্লিক করুন; read হয়ে badge কমবে (একাধিক tab থাকলে সবগুলোতেই sync হবে)
  4. Mark all — Mark all read চাপুন; সব read হবে আর badge 0
  5. Isolation — দ্বিতীয় tab-এ NEXT_PUBLIC_DEMO_TOKEN=token-demo-2 ব্যবহার করুন; একজনের notification অন্যজন দেখবে না
  6. Reconnection — server বন্ধ করলে footer Offline হবে; আবার চালু করলে reconnect হয়ে list ফিরে আসবে (in-memory সীমায়)
  7. Auth — ভুল token দিলে connect ব্যর্থ হবে (connect_error)
  8. XSS — server body-তে <b>x</b> পাঠালে এটি text হিসেবেই দেখাবে
StrictMode ও client

Next.js dev mode-এ React StrictMode effect দুইবার চালায় — তাই cleanup ঠিকভাবে লেখা জরুরি, নাহলে duplicate listener হবে। client component-এ অবশ্যই "use client" দিন ও socket কেবল effect-এ তৈরি করুন।

Troubleshooting

সমস্যাকারণ ও সমাধান
connect_error / unauthorizedauth.token পাঠানো হচ্ছে কি? token verifyToken-এ আছে কি?
connect_error / CORSserver-এর cors.origin-এ client origin (http://localhost:3000) দিন; \"*\" নয়
Connection refusedserver চালু আছে কি? http://localhost:4000/health খুলে দেখুন
Notification এসে দুবার দেখাচ্ছেduplicate listener বা merge ছাড়া append — merge id দিয়ে dedupe করে
অন্য user-এর notification দেখা যাচ্ছেকখনো io.emit ব্যবহার করা হয়েছে? সব emit io.to(userRoom).emit হতে হবে
NEXT_PUBLIC_SOCKET_URL কাজ করছে না.env.local বানানোর পর dev server restart করুন
Badge কমছে নাmark-read event সঠিক id পাঠাচ্ছে কি ও server emit করছে কি দেখুন
Type mismatchserver ও client একই @notify/shared type import করছে কি না দেখুন
Server restart-এ সব গায়েবin-memory store; নিচের production অংশ দেখুন

Production Considerations (Data Persistence)

এই teaching example আর production-এর মধ্যে বড় পার্থক্য:

  • In-memory notification server restart/crash-এ মুছে যায় — বর্তমান example-এ কোনো database নেই, তাই notification স্থায়ীভাবে সংরক্ষিত নয়।
  • Socket.IO delivery একা durable notification history দেয় না — offline থাকাকালীন notification হারাতে পারে।
  • production-এ notification ও তার read status database-এ রাখুন, এবং connect/reconnect-এ সেই table থেকে আনুন।

Database schema (উদাহরণ — PostgreSQL)

schema.sqlsql
sql
-- notifications টেবিল: notification ও read status দুটোই
CREATE TABLE notifications (
  id          TEXT PRIMARY KEY,
  user_id     TEXT NOT NULL,
  title       TEXT NOT NULL,
  body        TEXT NOT NULL,
  read        BOOLEAN NOT NULL DEFAULT FALSE,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_notifications_user_created
  ON notifications (user_id, created_at DESC);

server/src/store.ts (database-backed version, conceptual)

db querytypescript
typescript
// production-এ in-memory Map বাদ দিয়ে এই ধরনের query ব্যবহার করুন
export async function listForUser(userId: string) {
  const rows = await db.query(
    "SELECT * FROM notifications WHERE user_id = $1 " +
      "ORDER BY created_at DESC LIMIT 100",
    [userId],
  );
  return rows;
}

// reconnect-এর পর missed notification আনতে
// WHERE user_id = $1 AND created_at > $2
// authorization: প্রতিটি query-তে দর্শকের userId server-এর verified identity থেকে
Multi-instance scaling

একাধিক server instance চললে প্রতিটির room আলাদা — তাই io.to(user:<id>) সব node-এ পৌঁছাবে না। এর জন্য shared adapter (যেমন @socket.io/redis-adapter) লাগবে, আর notification store-ও common DB/Redis-এ রাখতে হবে (Best Practices)।

server/src/socket.ts (Redis adapter — production)

redis adaptertypescript
typescript
// একাধিক instance-এ room/emit ভাগ করতে shared adapter
import { createAdapter } from "@socket.io/redis-adapter";
import { createClient } from "redis";

const pub = createClient({ url: process.env.REDIS_URL });
const sub = pub.duplicate();
await Promise.all([pub.connect(), sub.connect()]);

io.adapter(createAdapter(pub, sub));

Security

  • কখনো broadcast নয় — private notification সবসময় io.to(userRoom(userId)).emit(...); কখনো io.emit(...) নয়।
  • room নাম client থেকে নয় — join শুধু server-side, verified userId থেকে তৈরি room-এ।
  • read operation authorize করুন — markRead/markAllRead সর্বদা verified userId-এর মধ্যে সীমাবদ্ধ; অন্য user-এর id দিলে "not found"।
  • client-এর পাঠানো userId বিশ্বাস করবেন না — handshake token/server session থেকেই identity নিন (Authentication)।
  • token log করবেন না — console.log(token) বা error-এ token print করবেন না।
  • Input validation — title/body length-limit ও trim করুন (এই store তাই করে)।
  • Rate limiting — spam notification ঠেকাতে throttle (Middleware)।
  • HTTPS/WSS — production-এ ট্রাফিক encrypt করুন (CORS Configuration)।
  • Safe rendering — {n.title} / {n.body} হিসেবে দেখান; dangerouslySetInnerHTML এড়িয়ে চলুন।

Limitations

in-memorytext
text
in-memory state:
- notification Map, read flag server process-এর memory-তে
- server restart / crash → সব হারায় (durable নয়)
- একাধিক node → room ও state প্রতিটিতে আলাদা
- Socket.IO delivery live-ই নিশ্চিত করে, history নয়
- production-এ DB + Redis adapter + auth verify লাগবে
History ও persistence নেই

এই example-এ কোনো database নেই — server notification সংরক্ষণ করে না। তাই প্রথম connect-এ আগের notification মিলতে পারে (একই process চললে), কিন্তু server restart বা page reload-এ স্থায়ীভাবে হারাবে।

Practice Task

  • project-টি উপরের সব file দিয়ে সম্পূর্ণভাবে locally চালান
  • notification-এ type (order, mention, system) যোগ করে icon/রং আলাদা করুন
  • mark-unread implement করুন — read flag আবার false করার authorization সহ
  • SQLite/Postgres দিয়ে notification persist করুন; reconnect-এ DB থেকে আনুন
  • handshake auth-এ আসল JWT verify দিয়ে verifyToken প্রতিস্থাপন করুন
  • user প্রতি unread count একটি আলাদা notification:count event দিয়ে sync করুন

সমাপ্তি

এই project-এ আপনি একটি সম্পূর্ণ full-stack real-time notification system বানিয়েছেন — handshake auth, user-specific rooms, live delivery, unread count, mark-as-read, reconnection-এ missed retrieval, validation ও safe rendering সহ। এখন এই ভিত্তি দিয়ে activity feed, alerts বা live dashboard বানাতে পারেন।

সম্পর্কিত অধ্যায়: Real-Time Chat Application · Next.js + Express Chat App · Rooms · Authentication · Broadcasting · Best Practices

শেষ প্যারায় নিজে practice করুন — কোড কপি করে নিজের এডিটরে চালিয়ে দেখুন।
আগেরBuild a Real-Time Chat App with Next.js, Express.js, and Socket.IOSocket.IO ডকুমেন্টেশন হোম
এই পেজে
Tech Tutor

বাংলায় ওয়েব ডেভেলপমেন্ট ডকুমেন্টেশন শিখুন — ধাপে ধাপে।

Homeসব ডকুমেন্টেশনQuick Startআমার নোটস

© 2026 Tech Tutor. বাংলায় শিখুন, বাংলায় গড়ুন।

mdanamolhasan1998@gmail.com