This document explains how Socket.IO is integrated with authentication (Passport + Sessions) on the backend and Zustand + React on the frontend, and how both sides work together end-to-end.
Browser (React + Zustand)
↓
Socket.IO Client (socket.io-client)
↓
HTTP Handshake (cookies + session)
↓
Socket.IO Server (Express + Passport)
Key points:
- One Express app
- One HTTP server
- One Socket.IO server
- Same session middleware for HTTP & sockets
This guarantees that login state is shared everywhere.
const app = express();
const server = http.createServer(app);
const io = new Server(server);Why this matters:
- Cookies sent to Express routes are also sent to Socket.IO
- Sessions and Passport work consistently
export const sessionMiddleware = session({
secret: 'the secret key',
resave: false,
saveUninitialized: true,
cookie: { maxAge: 30 * 24 * 60 * 60 * 1000 }
});What it does:
- Reads
connect.sidcookie - Loads user session
- Makes session available as
req.session
Used in Express:
app.use(sessionMiddleware);
app.use(passport.initialize());
app.use(passport.session());Result:
req.useris available on all authenticated HTTP routes
Before WebSocket starts, Socket.IO sends an HTTP request:
GET /socket.io/?EIO=4
Cookie: connect.sid=...
Authentication happens only here.
function onlyForHandshake(middleware) {
return (req, res, next) => {
const isHandshake = req._query.sid === undefined;
if (isHandshake) middleware(req, res, next);
else next();
};
}Why this is needed:
- Sessions work only on HTTP requests
- WebSocket packets are not HTTP
io.engine.use(onlyForHandshake(sessionMiddleware));
io.engine.use(onlyForHandshake(passport.initialize()));
io.engine.use(onlyForHandshake(passport.session()));
io.engine.use(
onlyForHandshake((req, res, next) => {
if (req.user) next();
else {
res.writeHead(401);
res.end();
}
})
);Execution flow:
- Session loaded from cookie
- Passport restores user
req.userchecked- Socket accepted or rejected
After this:
socket.request.user // authenticated userconst userSocketMap = {};
const userMap = {};Purpose:
userSocketMap: user → socketIduserMap: userId → user object
io.on('connection', (socket) => {
const user = socket.request.user;
userSocketMap[user.id] = socket.id;
userMap[user.id] = user;
io.emit('user_connected', { onlineUsers: Object.values(userMap) });
});Effect:
- User marked online
- All clients notified
socket.on('disconnect', () => {
delete userSocketMap[user.id];
delete userMap[user.id];
io.emit('user_disconnected', { onlineUsers: Object.values(userMap) });
});Effect:
- User removed from online list
- Clients update UI instantly
export const useStore = create((set, get) => ({
user: null,
socket: null,
onlineUsers: []
}));Why Zustand:
- Global socket state
- Survives route changes
- Easy real-time updates
const socket = io('http://localhost:3000', {
withCredentials: true
});Why withCredentials:
- Sends session cookie
- Enables backend authentication
socket.on('online_users', data => {
set({ onlineUsers: data.onlineUsers });
});
socket.on('user_connected', data => {
set({ onlineUsers: data.onlineUsers });
});
socket.on('user_disconnected', data => {
set({ onlineUsers: data.onlineUsers });
});Effect:
- Zustand updates state
- React re-renders automatically
const res = await axios.get('/api/auth/user', { withCredentials: true });If logged in:
setUser(res.data.user);
connectSocket();<Route path="/dashboard" element={user ? <Dashboard /> : <Navigate to="/login" />} />Ensures:
- UI access only for authenticated users
User logs in
→ session created
→ cookie stored
Socket connects
→ cookie sent
→ session loaded
→ passport restores user
→ socket accepted
User connects/disconnects
→ server broadcasts
→ clients update UI
- Secure (session-based auth)
- Efficient (auth once per connection)
- Scalable (works with Redis / DB stores)
- Production-grade (same as Slack / WhatsApp)
- Use Redis / DB session store
- Prevent multiple sockets per user (or support arrays)
- Disconnect socket on logout
- Handle reconnect logic
**This setup represents a clean, professional Socket.IO integration suitable for real