backend
Realtime messaging with NestJS gateways
Realtime messaging with NestJS gateways
The messaging between customers and organisations needs delivery without polling. Socket.io gives me a WebSocket connection with automatic long-polling fallback, plus a room abstraction on top.
@WebSocketGateway({ namespace: "messaging" })
export class MessagingGateway {
@SubscribeMessage("joinConversation")
handleJoin(client: Socket, conversationId: string) {
client.join(`conversation:${conversationId}`);
}
// emitting to the room reaches only sockets that joined it
this.server.to(`conversation:${conversationId}`).emit("newMessage", message);
}
A room is just Socket.io's grouping mechanism. Every socket connected to a conversation joins its room; emitting to the room reaches only those sockets. It maps cleanly onto conversation_participants — only participants get the events.
Auth on a WebSocket is different. The handshake doesn't carry headers like a normal request, so auth happens via a query param or auth payload on connection, or a cookie/token validated during the handshake, with the resolved user attached to the socket for every later handler. Get this wrong and either sockets are unauthenticated, or every handler re-implements its own check. (I also once had a race here: handleConnection is async, and a fast client could reach handleJoin before the token finished resolving. The fix was re-deriving the token from the handshake in handleJoin if the cached value wasn't there yet.)
The Redis adapter stops being optional at real scale. Room membership lives in one process's memory by default. With more than one instance, server.to(room).emit(...) only reaches sockets on that same instance, and nothing is thrown to say the message went nowhere. The adapter has every instance publish through a shared pub/sub channel.
A debugging note that cost me time once: sockets behave very differently under local dev hot-reload. A backend container restart drops every open connection, and the frontend needs reconnect logic that actually rejoins rooms, not just re-establishes the raw connection. A silent reconnect that forgets to rejoin looks exactly like "realtime stopped working" even though the connection itself is fine.