Claude Code for Socket.IO: Real-Time Bidirectional Communication — Claude Skills 360 Blog
Blog / Backend / Claude Code for Socket.IO: Real-Time Bidirectional Communication
Backend

Claude Code for Socket.IO: Real-Time Bidirectional Communication

Published: May 12, 2027
Read time: 7 min read
By: Claude Skills 360

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.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free