How Real-Time Collaboration Is Implemented with WebSocket Room-Based Trip Channels in TREK
TREK implements real-time collaboration using a room-based WebSocket architecture where clients join specific trip rooms, and the server broadcasts updates to all sockets in that room while excluding the originator to prevent echo-back.
The TREK repository (mauriceboe/TREK) uses a classic room-based WebSocket system to synchronize trip data across multiple users in real-time. This architecture ensures that every participant viewing the same trip receives instant updates when itinerary details, notes, or files change. Understanding how real-time collaboration WebSocket room-based trip channels work requires examining both the server-side room management and the client-side connection handling.
Server-Side Room Management and Broadcasting
WebSocket Server Setup and Authentication
The server initializes a single WebSocketServer that listens on the /ws endpoint through the setupWebSocket function in server/src/websocket.ts. When a client connects, it must present a one-time ws-token issued by the authentication service. After validation, the socket receives three critical pieces of metadata:
// https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts
socketId.set(nws, sid);
socketUser.set(nws, user);
socketRooms.set(nws, new Set());
These mappings track the unique socketId, the authenticated User object (socketUser), and an empty Set<number> called socketRooms that will store which trip rooms this connection has joined.
Join and Leave Actions
Clients communicate room membership through JSON messages with types join or leave, including a tripId parameter. The server validates permissions via canAccessTrip before updating its internal room registry:
if (msg.type === 'join' && msg.tripId) {
const tripId = Number(msg.tripId);
if (!canAccessTrip(tripId, user.id)) { /* handle unauthorized */ }
if (!rooms.has(tripId)) rooms.set(tripId, new Set());
rooms.get(tripId).add(nws);
socketRooms.get(nws).add(tripId);
}
The system maintains two essential data structures:
rooms: Map<number, Set<NomadWebSocket>>– Maps each trip ID to the set of connected sockets currently viewing that tripsocketRooms: WeakMap<NomadWebSocket, Set<number>>– Tracks which rooms each specific socket has joined for efficient cleanup
When a socket closes, the server iterates through socketRooms to call leaveRoom for every trip, ensuring the rooms map stays synchronized and memory is properly managed.
Broadcasting to Trip Rooms
The broadcast function in server/src/websocket.ts distributes updates to all participants in a trip room:
function broadcast(tripId, eventType, payload, excludeSid) {
// Iterates over rooms.get(tripId) and sends JSON payload
// Skips socket matching excludeSid to prevent echo-back
}
Services that mutate trip data—such as note updates or itinerary changes—call broadcast(tripId, eventType, payload, excludeSid) where the excludeSid parameter typically represents the originating client. This prevents users from receiving their own changes back while ensuring all other participants see updates instantly.
Client-Side WebSocket Manager
Connection Lifecycle and Reconnection
All browser code communicates through a singleton WebSocket instance defined in client/src/api/websocket.ts. The connection process follows a specific sequence:
- Token acquisition –
connect()fetches a temporary ws-token from/api/auth/ws-token - URL construction – Builds
/ws?token=...and opens the socket - Automatic reconnection – On disconnect, the client implements exponential back-off retry logic
- State resynchronization – After reconnecting, the client re-joins all active trips and optionally runs a
preReconnectHookto flush pending mutations before refetching data
Active Trip Tracking
The client maintains an activeTrips set containing every trip ID the user is currently viewing. When the socket opens or reopens, it automatically transmits join messages for each active trip:
// https://github.com/mauriceboe/TREK/blob/main/client/src/api/websocket.ts
export function joinTrip(tripId: number | string): void {
activeTrips.add(String(tripId));
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: 'join', tripId: String(tripId) }));
}
}
UI components register event handlers via addListener and removeListener functions. Incoming messages are parsed and dispatched to all registered listeners, enabling reactive updates across the application.
React Hook Integration
The useTripWebSocket hook in client/src/hooks/useTripWebSocket.ts provides a clean interface for React components to participate in real-time collaboration:
// https://github.com/mauriceboe/TREK/blob/main/client/src/hooks/useTripWebSocket.ts
export function useTripWebSocket(tripId) {
const tripStore = useTripStore();
useEffect(() => {
if (!tripId) return;
const handler = useTripStore.getState().handleRemoteEvent;
joinTrip(tripId);
addListener(handler);
const collabFileSync = (event) => {
if (event?.type === 'collab:note:deleted' || event?.type === 'collab:note:updated') {
tripStore.loadFiles?.(tripId);
}
};
addListener(collabFileSync);
return () => {
leaveTrip(tripId);
removeListener(handler);
removeListener(collabFileSync);
};
}, [tripId]);
}
This hook manages the component lifecycle by joining the trip room on mount and leaving it on unmount. It also sets up specialized listeners for file synchronization events, ensuring that changes to shared notes or documents from other users trigger immediate UI updates.
End-to-End Collaboration Flow
Understanding the complete real-time collaboration WebSocket room-based trip channels implementation requires seeing how the pieces interact:
- Application startup – The client calls
connect()to establish the WebSocket connection - Trip navigation – When a user opens a trip,
useTripWebSocket(tripId)executes and sends ajoinmessage - Server validation – The server verifies permissions via
canAccessTripand adds the socket to the appropriate room in theroomsmap - Data mutation – When any participant updates trip data, the relevant service calls
broadcast(tripId, eventType, payload, originatorSid) - Client update – All other sockets in the room receive the payload, triggering
handleRemoteEventor file-sync callbacks to update the UI - Network resilience – If connectivity drops, automatic reconnection logic reestablishes the socket and re-joins all active trips, optionally refreshing data to ensure consistency
Summary
- TREK uses a room-based WebSocket architecture where each trip represents a distinct room managed through
server/src/websocket.ts - Dual mapping strategy uses
rooms(trip-to-sockets) andsocketRooms(socket-to-trips) for efficient membership tracking and cleanup - Authentication requires one-time ws-tokens for connection establishment, with permission checks via
canAccessTripbefore allowing room entry - Client singleton pattern in
client/src/api/websocket.tshandles connection management, automatic reconnection with exponential back-off, and active trip tracking - React integration through
useTripWebSocketprovides declarative subscription management and automatic cleanup on component unmount - Broadcast exclusion prevents echo-back by skipping the originator socket when distributing updates to room participants
Frequently Asked Questions
How does the server prevent unauthorized users from joining trip rooms?
The server validates every join request using the canAccessTrip(tripId, user.id) function before adding the socket to the room. This check occurs in server/src/websocket.ts when processing join messages, ensuring only users with proper permissions can receive real-time updates for a specific trip.
What happens when a user's network connection drops temporarily?
The client-side WebSocket manager implements automatic reconnection with exponential back-off. Upon reconnecting, it references the activeTrips set to automatically re-send join messages for all trips the user was viewing. An optional preReconnectHook can flush pending mutations and trigger data refetching to ensure consistency after reconnection.
How does the system handle file synchronization between collaborators?
The useTripWebSocket hook registers specialized listeners for file-related events such as collab:note:deleted and collab:note:updated. When these events arrive from the server, they trigger tripStore.loadFiles(tripId) to refresh the file list, ensuring all participants see document changes immediately without manual refreshes.
Why does the broadcast function exclude the originator socket?
The broadcast function accepts an excludeSid parameter that specifies a socket ID to skip when sending messages. This prevents users from receiving echoes of their own changes, reducing network traffic and eliminating the need for clients to filter out their own updates while maintaining instantaneous synchronization for all other room participants.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →