Skip to content

WebSocket gateway

The project uses Socket.io with a session-based authentication model. WebSocket connections are persistent, so the starter authenticates once during the handshake and trusts the connection afterwards. This keeps per-message latency low, which is important for real-time systems. The trade-off is that invalidating a user who is already connected is harder; you have to disconnect them explicitly rather than relying on a token expiring on every message.

NestJS applies the connection logic of one gateway to all gateways on the same namespace. The starter uses this to centralize authentication in a base gateway and extend it for domain-specific features.

Create a base gateway that handles authentication in handleConnection:

export class WSManager implements OnGatewayInit, OnGatewayConnection {
@Inject(AuthenticationService)
private readonly authService: AuthenticationService;
handleConnection(client: Socket, ...args: any[]) {
const user = this.authService.authenticateHandshake(client.handshake);
if (!user) {
client.disconnect(true);
return;
}
client.user = user;
}
}

Use property-based injection on the parent class so subclasses do not have to pass dependencies through their constructors.

Extend it for each feature:

@WebSocketGateway({ namespace: '/notifications' })
export class NotificationGateway extends WSManager {
@SubscribeMessage('subscribe-to-notifications')
handleSubscription(client: Socket) {
// client.user is already set
}
}

If a namespace needs extra connection logic, call super.handleConnection() and add your own:

@WebSocketGateway({ namespace: '/chat' })
export class ChatGateway extends WSManager {
constructor(private readonly chatService: ChatService) {
super();
}
handleConnection(client: Socket, ...args: any[]) {
super.handleConnection(client, ...args);
const room = this.chatService.resolveRoom(client.handshake);
client.join(room);
}
}

The generated project ships with a single WsConnectionsManagerGateway on the default namespace. It reads the session cookie from the handshake, looks up the Redis session key sess:<sessionId>, attaches the user to the socket, and joins a per-user room.

src/core/websocket/ws-connections-manager.gateway.ts
async handleConnection(client: Socket) {
const sessionId = this.extractSessionIdFromSocket(client);
if (!sessionId) {
client.disconnect(true);
return;
}
const userId = await this.resolveUserIdFromSession(sessionId);
if (!userId) {
client.disconnect(true);
return;
}
const user = await this.userService.findById(userId);
if (!user) {
client.disconnect(true);
return;
}
client['user'] = user;
await client.join(`user_${user.id}`);
}

The starter includes @socket.io/redis-adapter in its dependencies but does not wire it by default. To scale across multiple Node.js processes, create the Redis adapter in the gateway’s afterInit or in a dedicated module and attach it to the server.

Guards are still useful for authorization (RBAC) on specific messages. You can guard a @SubscribeMessage handler with a custom guard that checks client.user or roles. The connection itself is already authenticated, so guards only need to decide if the user may perform the action.