Socket.io
WebSocket server, rooms, auth, and client integration.
Socket.io handles realtime bidirectional communication — live notifications, chat, presence, and dashboard updates. It runs alongside Express on the same HTTP server.
Install
pnpm add socket.io
pnpm add -D @types/corsServer setup
// src/server.ts
import { createServer } from "http";
import { createApp } from "./app";
import { initSocket } from "./socket";
const PORT = process.env.PORT ?? 4000;
const app = createApp();
const httpServer = createServer(app);
initSocket(httpServer);
httpServer.listen(PORT, () => {
console.log(`API + Socket.io on http://localhost:${PORT}`);
});// src/socket/index.ts
import { Server } from "socket.io";
import type { Server as HttpServer } from "http";
import { socketAuth } from "./auth.middleware";
import { registerNotificationHandlers } from "./handlers/notifications.handler";
export function initSocket(httpServer: HttpServer) {
const io = new Server(httpServer, {
cors: {
origin: process.env.CORS_ORIGIN?.split(","),
credentials: true,
},
path: "/socket.io",
});
io.use(socketAuth);
io.on("connection", (socket) => {
const userId = socket.data.user.sub;
// Join a personal room for targeted events
socket.join(`user:${userId}`);
registerNotificationHandlers(io, socket);
socket.on("disconnect", () => {
console.log(`Socket disconnected: ${userId}`);
});
});
return io;
}Socket authentication
Reuse Cognito JWT verification from the REST API:
// src/socket/auth.middleware.ts
import { Socket } from "socket.io";
import { verifyCognitoToken } from "../lib/cognito";
export async function socketAuth(socket: Socket, next: (err?: Error) => void) {
const token = socket.handshake.auth.token as string;
if (!token) return next(new Error("Authentication required"));
try {
const payload = await verifyCognitoToken(token);
socket.data.user = { sub: payload.sub!, email: payload.email as string };
next();
} catch {
next(new Error("Invalid token"));
}
}Event handlers
// src/socket/handlers/notifications.handler.ts
import { Server, Socket } from "socket.io";
export function registerNotificationHandlers(io: Server, socket: Socket) {
socket.on("notification:read", async (notificationId: string) => {
// Mark as read in DB
await markNotificationRead(notificationId, socket.data.user.sub);
socket.emit("notification:updated", { id: notificationId, read: true });
});
}
// Emit from a service after a REST action
export function notifyUser(io: Server, userId: string, event: string, data: unknown) {
io.to(`user:${userId}`).emit(event, data);
}Client connection (Next.js)
import { io } from "socket.io-client";
const socket = io(process.env.NEXT_PUBLIC_API_URL!, {
auth: { token: accessToken },
path: "/socket.io",
});
socket.on("connect", () => console.log("Connected"));
socket.on("notification:new", (data) => showToast(data));Rooms pattern
| Room | Purpose | Join trigger |
|---|---|---|
user:{id} | Personal notifications | On connect |
project:{id} | Project collaboration | On project page load |
workspace:{id} | Workspace-wide events | On workspace select |
Emitting from Express services
// src/services/projects.service.ts
import { getIO } from "../socket/io-instance";
export async function createProject(data: CreateProjectInput) {
const project = await db.insert(projects).values(data).returning();
// Notify workspace members via Socket.io
getIO().to(`workspace:${data.workspaceId}`).emit("project:created", project);
return project;
}