Socket.IO adds reliable, event-based bidirectional communication over WebSockets — io.on("connection", (socket) => ...) handles new connections. socket.emit("event", data) sends to one client; io.emit broadcasts to all; io.to(room).emit sends to a room. socket.join("room-id") / socket.leave() manage rooms. socket.on("event", callback) handles incoming events. Acknowledgements: socket.emit("event", data, (response) => ...) with callback(result) on the server. io.use((socket, next) => ...) is middleware for auth. socket.data.userId stores per-socket state. Namespaces /chat and /notifications split concerns. @socket.io/redis-adapter connects multiple server instances. TypeScript: define ServerToClientEvents, ClientToServerEvents, SocketData interfaces. socket.handshake.auth.token carries the JWT from the client. Claude Code generates Socket.IO chat servers, live collaboration, notification hubs, game lobbies, and presence tracking.
CLAUDE.md for Socket.IO
## Socket.IO Stack
- Version: socket.io >= 4.7, socket.io-client >= 4.7, @socket.io/redis-adapter >= 8.3
- Server: const io = new Server(httpServer, { cors: { origin: process.env.ALLOWED_ORIGIN, credentials: true } })
- Auth: io.use(async (socket, next) => { const token = socket.handshake.auth.token; ... socket.data.userId = userId; next() })
- Rooms: socket.join(`user:${userId}`); io.to(`user:${userId}`).emit("notification", data)
- Types: const io = new Server<ClientToServer, ServerToClient, {}, SocketData>(httpServer, options)
- Redis: io.adapter(createAdapter(pubClient, subClient)) — horizontal scaling
- Client: const socket = io({ auth: { token }, autoConnect: true, reconnectionAttempts: 5 })
Server Setup
// lib/socket/server.ts — typed Socket.IO server
import { Server } from "socket.io"
import { createServer } from "http"
import { createAdapter } from "@socket.io/redis-adapter"
import { createClient } from "redis"
import { verifyAccessToken } from "@/lib/auth/jwt"
import { db } from "@/lib/db"
// ── Typed event interfaces ─────────────────────────────────────────────────
interface ServerToClientEvents {
"chat:message": (msg: ChatMessage) => void
"chat:typing": (data: { userId: string; roomId: string; isTyping: boolean }) => void
"user:online": (data: { userId: string }) => void
"user:offline": (data: { userId: string }) => void
"notification": (notification: Notification) => void
"error": (error: { message: string }) => void
}
interface ClientToServerEvents {
"chat:send": (data: { roomId: string; content: string }, ack: (result: { messageId: string } | { error: string }) => void) => void
"chat:typing": (data: { roomId: string; isTyping: boolean }) => void
"room:join": (roomId: string, ack: (result: { ok: boolean } | { error: string }) => void) => void
"room:leave": (roomId: string) => void
}
interface SocketData {
userId: string
name: string
role: "user" | "admin"
}
type ChatMessage = {
id: string
roomId: string
userId: string
authorName: string
content: string
createdAt: string
}
type Notification = {
id: string
type: string
message: string
createdAt: string
}
export type TypedServer = Server<ClientToServerEvents, ServerToClientEvents, {}, SocketData>
export type TypedSocket = Parameters<Parameters<TypedServer["on"]>[1]>[0]
// ── Redis adapter for horizontal scaling ──────────────────────────────────
async function createRedisAdapter() {
const pubClient = createClient({ url: process.env.REDIS_URL })
const subClient = pubClient.duplicate()
await Promise.all([pubClient.connect(), subClient.connect()])
return createAdapter(pubClient, subClient)
}
// ── Build IO server ────────────────────────────────────────────────────────
export async function buildSocketServer(httpServer: ReturnType<typeof createServer>): Promise<TypedServer> {
const io = new Server<ClientToServerEvents, ServerToClientEvents, {}, SocketData>(httpServer, {
cors: {
origin: process.env.ALLOWED_ORIGIN ?? "http://localhost:3000",
credentials: true,
},
transports: ["websocket", "polling"],
pingInterval: 25_000,
pingTimeout: 20_000,
})
// Redis adapter for multi-instance
if (process.env.REDIS_URL) {
io.adapter(await createRedisAdapter())
}
// ── Auth middleware ──────────────────────────────────────────────────────
io.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token as string | undefined
if (!token) return next(new Error("Authentication required"))
const payload = await verifyAccessToken(token)
const user = await db.user.findUnique({
where: { id: payload.userId },
select: { id: true, name: true, role: true },
})
if (!user) return next(new Error("User not found"))
socket.data.userId = user.id
socket.data.name = user.name
socket.data.role = user.role as "user" | "admin"
next()
} catch {
next(new Error("Invalid token"))
}
})
// ── Connection handler ───────────────────────────────────────────────────
io.on("connection", (socket) => {
const { userId, name } = socket.data
// Auto-join user's personal room for direct notifications
socket.join(`user:${userId}`)
// Presence — broadcast online status
socket.broadcast.emit("user:online", { userId })
// ── Room join ────────────────────────────────────────────────────────
socket.on("room:join", async (roomId, ack) => {
try {
// Verify user has access to room
const member = await db.roomMember.findFirst({
where: { roomId, userId },
})
if (!member) return ack({ error: "Access denied" })
await socket.join(`room:${roomId}`)
ack({ ok: true })
} catch {
ack({ error: "Failed to join room" })
}
})
socket.on("room:leave", (roomId) => {
socket.leave(`room:${roomId}`)
})
// ── Chat message ─────────────────────────────────────────────────────
socket.on("chat:send", async ({ roomId, content }, ack) => {
try {
if (!content.trim() || content.length > 2000) {
return ack({ error: "Invalid message" })
}
// Verify room membership
const inRoom = socket.rooms.has(`room:${roomId}`)
if (!inRoom) return ack({ error: "Not in room" })
// Persist
const message = await db.chatMessage.create({
data: { roomId, userId, content: content.trim() },
select: { id: true, createdAt: true },
})
const payload: ChatMessage = {
id: message.id,
roomId,
userId,
authorName: name,
content: content.trim(),
createdAt: message.createdAt.toISOString(),
}
// Broadcast to room (including sender)
io.to(`room:${roomId}`).emit("chat:message", payload)
ack({ messageId: message.id })
} catch {
ack({ error: "Failed to send message" })
}
})
// ── Typing indicator ─────────────────────────────────────────────────
socket.on("chat:typing", ({ roomId, isTyping }) => {
socket.to(`room:${roomId}`).emit("chat:typing", { userId, roomId, isTyping })
})
// ── Disconnect ────────────────────────────────────────────────────────
socket.on("disconnect", () => {
io.emit("user:offline", { userId })
})
})
return io
}
// Send notification to a specific user (external utility)
export function notifyUser(io: TypedServer, userId: string, notification: Notification) {
io.to(`user:${userId}`).emit("notification", notification)
}
React Client Hook
// hooks/useSocket.ts — typed client-side socket hook
"use client"
import { useEffect, useRef, useCallback } from "react"
import { io, type Socket } from "socket.io-client"
import type { ServerToClientEvents, ClientToServerEvents } from "@/lib/socket/types"
type TypedSocket = Socket<ServerToClientEvents, ClientToServerEvents>
let sharedSocket: TypedSocket | null = null
export function useSocket(token: string | null) {
const socketRef = useRef<TypedSocket | null>(null)
useEffect(() => {
if (!token) return
// Reuse existing connection
if (sharedSocket?.connected) {
socketRef.current = sharedSocket
return
}
const socket = io(process.env.NEXT_PUBLIC_SOCKET_URL ?? "", {
auth: { token },
transports: ["websocket"],
reconnectionAttempts: 5,
reconnectionDelay: 1000,
autoConnect: true,
})
socket.on("connect_error", (err) => {
console.error("[Socket] connection error:", err.message)
})
sharedSocket = socket
socketRef.current = socket
return () => {
// Don't disconnect on unmount — keep shared connection alive
socketRef.current = null
}
}, [token])
const joinRoom = useCallback((roomId: string): Promise<{ ok: boolean }> => {
return new Promise((resolve, reject) => {
if (!socketRef.current) return reject(new Error("Not connected"))
socketRef.current.emit("room:join", roomId, (result) => {
if ("error" in result) reject(new Error(result.error))
else resolve(result)
})
})
}, [])
const sendMessage = useCallback(
(roomId: string, content: string): Promise<{ messageId: string }> => {
return new Promise((resolve, reject) => {
if (!socketRef.current) return reject(new Error("Not connected"))
socketRef.current.emit("chat:send", { roomId, content }, (result) => {
if ("error" in result) reject(new Error(result.error))
else resolve(result)
})
})
},
[],
)
return { socket: socketRef.current, joinRoom, sendMessage }
}
For the LiveKit alternative when full-duplex video and audio conferencing with WebRTC track management is needed alongside data channels — LiveKit is purpose-built for media streams while Socket.IO handles general-purpose event messaging without media capabilities, see the LiveKit guide. For the Ably/Pusher alternative when a hosted WebSocket service with no server management, guaranteed delivery, presence channels, and global edge network are preferred over running your own Socket.IO server — Ably and Pusher trade self-hosting flexibility for operational simplicity, see the Pusher guide. The Claude Skills 360 bundle includes Socket.IO skill sets covering rooms, namespaces, Redis scaling, and typed events. Start with the free tier to try real-time WebSocket generation.