WebSockets add real-time capabilities to your app: live collaboration, notifications, presence indicators, and streaming data. Here's how to implement them correctly in a Next.js production environment.
The Architecture Problem
Next.js serverless functions are stateless — they can't maintain a persistent WebSocket connection. You need a dedicated WebSocket server running alongside your Next.js app.
Options:
- Socket.io server: Standalone Node.js process
- Ably / Pusher: Managed WebSocket service
- Partykit: Built for real-time Next.js apps
- Supabase Realtime: If you're already on Supabase
For self-hosted control: Socket.io. For zero-ops: Ably.
Socket.io Server Setup
// server/socket.ts
import { Server } from 'socket.io'
import { createServer } from 'http'
import { verify } from 'jsonwebtoken'
const httpServer = createServer()
const io = new Server(httpServer, {
cors: {
origin: process.env.NEXT_PUBLIC_APP_URL,
credentials: true,
},
})
// Auth middleware
io.use((socket, next) => {
const token = socket.handshake.auth.token
try {
const payload = verify(token, process.env.NEXTAUTH_SECRET!) as { userId: string }
socket.data.userId = payload.userId
next()
} catch {
next(new Error('Authentication failed'))
}
})
io.on('connection', (socket) => {
const { userId } = socket.data
// Join user's personal room
socket.join(`user:${userId}`)
socket.on('join:project', (projectId: string) => {
socket.join(`project:${projectId}`)
socket.to(`project:${projectId}`).emit('user:joined', { userId })
})
socket.on('cursor:move', ({ projectId, x, y }) => {
socket.to(`project:${projectId}`).emit('cursor:update', { userId, x, y })
})
socket.on('disconnect', () => {
io.emit('user:left', { userId })
})
})
httpServer.listen(3001)
Client Connection
// lib/socket.ts
import { io, Socket } from 'socket.io-client'
import { getSession } from 'next-auth/react'
let socket: Socket | null = null
export async function getSocket(): Promise<Socket> {
if (socket?.connected) return socket
const session = await getSession()
if (!session) throw new Error('Not authenticated')
socket = io(process.env.NEXT_PUBLIC_SOCKET_URL!, {
auth: { token: session.accessToken },
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5,
})
return new Promise((resolve, reject) => {
socket!.on('connect', () => resolve(socket!))
socket!.on('connect_error', reject)
})
}
React Hook
function useProjectPresence(projectId: string) {
const [onlineUsers, setOnlineUsers] = useState<string[]>([])
useEffect(() => {
let sock: Socket
getSocket().then(s => {
sock = s
sock.emit('join:project', projectId)
sock.on('user:joined', ({ userId }) => {
setOnlineUsers(prev => [...new Set([...prev, userId])])
})
sock.on('user:left', ({ userId }) => {
setOnlineUsers(prev => prev.filter(id => id !== userId))
})
})
return () => {
sock?.emit('leave:project', projectId)
sock?.off('user:joined')
sock?.off('user:left')
}
}, [projectId])
return onlineUsers
}
Pushing Notifications from Server
// From your Next.js API routes -- push to specific users
async function notifyUser(userId: string, event: string, data: unknown) {
await fetch(`${process.env.SOCKET_SERVER_URL}/emit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.SOCKET_SERVER_SECRET}`,
},
body: JSON.stringify({ room: `user:${userId}`, event, data }),
})
}
// Example: notify user when their export is ready
await notifyUser(userId, 'export:ready', { downloadUrl })
Scaling WebSockets
Multiple Socket.io instances need a shared adapter so events cross server boundaries:
import { createAdapter } from '@socket.io/redis-adapter'
import { createClient } from 'redis'
const pubClient = createClient({ url: process.env.REDIS_URL })
const subClient = pubClient.duplicate()
await Promise.all([pubClient.connect(), subClient.connect()])
io.adapter(createAdapter(pubClient, subClient))
The AI SaaS Starter at whoffagents.com includes Socket.io setup with auth middleware, presence hooks, and Redis adapter configuration. $99 one-time.
Top comments (0)