WebSocket Architecture
Planner uses Socket.IO for real-time bidirectional communication. There are three separate WebSocket namespaces, each handling a different domain.
Overview
| Namespace | Purpose | Auth | Direction |
|---|---|---|---|
/notifications | Push notifications | JWT | Server → Client |
/chat | Real-time messaging | JWT | Bidirectional |
/video | Video room signaling | JWT | Bidirectional |
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.
| Direction | Event | Payload | Description |
|---|---|---|---|
| Server → Client | notification:new | Notification object | New notification for the user |
Users automatically join user:<id> room on connection.
/chat
Bidirectional messaging for real-time conversations.
| Direction | Event | Payload | Description |
|---|---|---|---|
| Client → Server | join | conversationId | Join conversation room |
| Client → Server | leave | conversationId | Leave conversation room |
| Client → Server | typing | conversationId | Typing indicator |
| Server → Client | chat: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.
| Direction | Event | Payload | Description |
|---|---|---|---|
| Client → Server | join | roomId | Join room (room:<id>) |
| Client → Server | leave | roomId | Leave room |
| Client → Server | signal | WebRTC signal | Signaling relay |
| Server → Client | video:participant_joined | Participant data | User joined |
| Server → Client | video:participant_left | Participant data | User left |
| Server → Client | video:room_updated | Room data | Room state changed |
| Server → Client | video:call_started | Room data | Instant 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 withthis.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
/videonamespace