How TREK Uses WebSockets for Real-Time Collaboration and Room-Based Trip Channels

TREK enables real-time collaboration by authenticating WebSocket connections with ephemeral JWT tokens, grouping sockets into trip-specific rooms on the server, and broadcasting updates through typed events while a singleton client manager handles automatic reconnection and state recovery.

TREK implements a lightweight, JWT-authenticated WebSocket layer to synchronize trip data instantly across all connected clients. The architecture consists of three integrated components: a WebSocket server that manages authentication and room-based messaging, a client-side singleton that maintains the connection and handles reconnection logic, and a sync layer that applies incoming events to the local state. This guide examines the implementation details found in the mauriceboe/TREK repository, covering everything from ephemeral token handshakes to trip-scoped broadcast channels.

WebSocket Architecture Overview

The real-time collaboration system in TREK is built on three primary components that work together to maintain consistency across clients.

WebSocket Server (server/src/websocket.ts): Creates the ws server, validates ephemeral tokens, assigns unique socket IDs, manages trip-based rooms, and exposes broadcasting helpers including broadcast and broadcastToUser.

Client Manager (client/src/api/websocket.ts): A singleton that opens a single WebSocket connection, automatically reconnects after network interruptions, joins and leaves trip rooms, and forwards incoming events to registered listeners via addListener and removeListener.

Sync Layer (client/src/sync/tripSyncManager.ts): Integrates with the client manager to queue mutations and refetch data after reconnections, ensuring the UI remains synchronized with the server state.

Server-Side Authentication and Room Management

The server implementation in server/src/websocket.ts handles connection lifecycle, authentication, and message routing through trip-specific channels.

Ephemeral Token Authentication

Every WebSocket connection begins with a short-lived token obtained via POST /api/auth/ws-token. The client retrieves this token through the fetchWsToken function before initiating the handshake.

On the server, the setupWebSocket function validates the token using consumeEphemeralTokenWithMeta (lines 72-98). If the token is invalid, expired, or the user's password version has changed, the server immediately closes the connection with code 4001 and terminates the session.

Socket Identification and Rate Limiting

Upon successful authentication, each socket receives a monotonically increasing socketId (lines 106-108). The server enforces rate limiting through a per-socket message counter that restricts clients to WS_MSG_LIMIT = 30 messages per 10-second window, preventing abuse of the real-time channel.

Trip-Based Room Logic

TREK organizes WebSocket connections into rooms scoped to specific trips using a rooms Map that associates each tripId with a Set of active sockets.

When a client sends a {type: 'join', tripId} message, the server verifies the user's access rights via canAccessTrip before adding the socket to the appropriate room (lines 42-54). The leaveRoom helper (lines 56-60) removes sockets when they explicitly leave or when the connection closes (lines 62-70), ensuring room membership always reflects active participants.

Broadcasting Helpers

The server exposes two primary broadcasting functions used by service layer code:

broadcast(tripId, eventType, payload, excludeSid?) (lines 88-102): Iterates through all sockets in a trip's room and sends a JSON payload. The optional excludeSid parameter prevents echoing updates back to the originating socket, reducing redundant UI updates on the sender's device.

broadcastToUser(userId, payload, excludeSid?) (lines 105-118): Walks all connected sockets, selects those belonging to the specific user, and delivers the message directly. This enables user-specific notifications across multiple devices.

Client-Side Connection and Reconnection

The client implementation in client/src/api/websocket.ts manages the WebSocket lifecycle as a singleton, ensuring only one connection exists per application instance while providing robust reconnection logic.

Singleton Connection Manager

The client exports a singleton that maintains a single WebSocket instance. Key methods include:

  • connect(): Initiates the connection using the ephemeral WS token
  • joinTrip(tripId): Sends a join request to the server and tracks the trip in activeTrips
  • leaveTrip(tripId): Removes the client from the trip room locally and notifies the server
  • addListener(callback): Registers functions to receive parsed messages from the server
  • removeListener(callback): Cleans up listeners to prevent memory leaks

Automatic Reconnection and State Recovery

The client handles network interruptions through a sophisticated reconnection mechanism. When the connection drops and reconnects (onopen), the client automatically re-joins all trips stored in activeTrips (lines 111-135).

Before reconnection completes, the preReconnectHook executes any pending mutations from the queue, ensuring local changes are persisted before fetching fresh data. If a refetchCallback is set, it triggers a full data refresh for each active trip, guaranteeing that the client state converges with the server after network disruptions.

End-to-End Data Flow

Understanding how these components interact clarifies how TREK maintains real-time consistency:

  1. Initial Connection: The client calls connect(), fetches a WS token via fetchWsToken, and opens ws://…/ws?token=…. The server validates the token, assigns a socketId, and sends {type: 'welcome', socketId}.

  2. Joining a Trip: When a user opens a trip view, the client calls joinTrip(tripId), sending {type: 'join', tripId} to the server. The server verifies access via canAccessTrip and adds the socket to rooms[tripId].

  3. Broadcasting Changes: When any user modifies trip data (e.g., adding an itinerary item), the server persists the change and calls broadcast(tripId, 'itinerary:added', {itemId, data}, socketId), excluding the sender.

  4. Receiving Updates: All clients in the room receive the payload, triggering registered listeners that update local stores via the sync layer, causing immediate UI reflection.

Implementation Examples

Server: Broadcasting Trip Updates

Service layer code utilizes the broadcast helpers to push changes to all trip participants:

// src/services/tripService.ts (simplified)
import { broadcast } from '../../src/websocket';

// After persisting a new itinerary item...
broadcast(tripId, 'itinerary:added', { itemId, data }, /*excludeSid=*/ socketId);

The excludeSid parameter prevents the update from echoing back to the originating client, avoiding redundant work.

Client: Handling Real-Time Events

Components subscribe to events through the listener API:

import { addListener, joinTrip } from '@/api/websocket';
import { useTripStore } from '@/store/trip';

// Register once in a top-level component
addListener((msg) => {
  if (msg.type === 'itinerary:added' && msg.tripId) {
    useTripStore().applyRemoteAdd(msg.tripId, msg.itemId, msg.data);
  }
});

// When navigating to a trip
joinTrip(currentTripId);

Reconnection Hooks

Before reconnecting, flush pending mutations and prepare to refetch:

import { setPreReconnectHook, setRefetchCallback } from '@/api/websocket';

setPreReconnectHook(() => mutationQueue.flush());
setRefetchCallback((tripId) => tripStore.refetch(tripId));

Summary

  • TREK WebSockets real-time collaboration relies on ephemeral JWT tokens for authentication, validated in server/src/websocket.ts before establishing the connection.
  • The server organizes connections into trip-specific rooms using a Map (tripId → Set<socket>), enabling targeted broadcasts via broadcast() and broadcastToUser().
  • Rate limiting enforces 30 messages per 10 seconds per socket to prevent abuse.
  • The singleton client manager in client/src/api/websocket.ts handles connection lifecycle, automatic reconnection, and room membership.
  • State recovery ensures consistency after reconnections by flushing mutation queues and refetching trip data before re-joining rooms.
  • UI components receive updates through the listener pattern, allowing the sync layer to merge remote changes instantly.

Frequently Asked Questions

How does TREK authenticate WebSocket connections?

TREK uses ephemeral JWT tokens fetched via POST /api/auth/ws-token before connection establishment. The server validates these tokens using consumeEphemeralTokenWithMeta in server/src/websocket.ts, and rejects connections with code 4001 if the token is invalid or the user's credentials have changed.

What happens when a user joins a trip channel?

When joinTrip(tripId) is called on the client, it sends a {type: 'join', tripId} message to the server. The server verifies the user has access via canAccessTrip, then adds the socket to a room specific to that trip ID. This allows the user to receive real-time updates whenever other participants modify the trip.

How does TREK handle WebSocket reconnection?

The client singleton automatically reconnects when the connection drops. Before reopening, it executes a preReconnectHook to flush pending mutations. After reconnecting, it re-joins all activeTrips and triggers refetchCallback to synchronize state, ensuring the client reflects any changes that occurred during the disconnection.

What is the purpose of the excludeSid parameter in broadcast functions?

The excludeSid parameter in broadcast(tripId, eventType, payload, excludeSid) prevents the server from sending the update back to the originating socket. This optimization reduces network traffic and prevents the sender's UI from redundantly processing its own changes.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →