Skip to content

WebSocket Architecture

Planner uses Socket.IO for real-time bidirectional communication. There are three separate WebSocket namespaces, each handling a different domain.

Overview

NamespacePurposeAuthDirection
/notificationsPush notificationsJWTServer → Client
/chatReal-time messagingJWTBidirectional
/videoVideo room signalingJWTBidirectional

Authentication

All WebSocket connections authenticate via handshake.auth.token — a JWT access token passed during the Socket.IO handshake. The server validates the token using JwtService and extracts the user ID.

typescript
// Client connection
const socket = io('/notifications', {
  auth: { token: accessToken }
})

Namespaces

/notifications

Server-push only — clients listen for events, no client messages.

DirectionEventPayloadDescription
Server → Clientnotification:newNotification objectNew notification for the user

Users automatically join user:<id> room on connection.

/chat

Bidirectional messaging for real-time conversations.

DirectionEventPayloadDescription
Client → ServerjoinconversationIdJoin conversation room
Client → ServerleaveconversationIdLeave conversation room
Client → ServertypingconversationIdTyping indicator
Server → Clientchat:typing{ conversationId, userId }Another user is typing

Messages are sent via REST (POST /api/chats/:id/messages), real-time delivery is handled by sendToConversation() on the server. Users join both user:<id> and conv:<id> rooms.

/video

Signaling and room events for video conferencing.

DirectionEventPayloadDescription
Client → ServerjoinroomIdJoin room (room:<id>)
Client → ServerleaveroomIdLeave room
Client → ServersignalWebRTC signalSignaling relay
Server → Clientvideo:participant_joinedParticipant dataUser joined
Server → Clientvideo:participant_leftParticipant dataUser left
Server → Clientvideo:room_updatedRoom dataRoom state changed
Server → Clientvideo:call_startedRoom dataInstant call started

Room Structure

Notifications:  user:<userId>
Chat:           user:<userId>, conv:<conversationId>
Video:          room:<roomId>

Frontend Integration

Notification Listener (in App.vue)

typescript
const socket = io('/notifications', { auth: { token } })
socket.on('notification:new', (notification) => {
  notificationsStore.addNotification(notification)
})

Chat Store

typescript
const socket = io('/chat', { auth: { token } })
socket.on('chat:typing', ({ conversationId, userId }) => { /* ... */ })
socket.emit('join', conversationId)
socket.emit('typing', conversationId)

Video Store

typescript
const socket = io('/video', { auth: { token } })
socket.on('video:participant_joined', (data) => { /* ... */ })
socket.on('video:participant_left', (data) => { /* ... */ })
socket.emit('join', roomId)

Key Implementation Details

  • NotificationsGateway.sendToUser() guards with this.server? check (server is undefined during testing)
  • Notifications are fire-and-forget — NotificationsService.create() is not awaited
  • Chat messages use REST for persistence, WebSocket for real-time delivery
  • Video signaling relays WebRTC offers/answers/ICE candidates through the /video namespace