How Real-Time WebSocket Collaboration Works in TREK: A Technical Deep Dive

TREK implements real-time WebSocket collaboration through a JWT-authenticated, room-based architecture that synchronizes trip data across clients using ephemeral tokens, automatic reconnection, and targeted broadcasting.

TREK is a trip planning application that keeps multiple participants instantly synchronized through a lightweight WebSocket layer. The real-time collaboration system ensures that any change made by one user—whether adding an itinerary item or updating a todo—immediately propagates to all other clients viewing the same trip. This article examines the complete implementation based on the source code in the mauriceboe/TREK repository.

Architecture Overview

The WebSocket collaboration system consists of three tightly integrated components that handle authentication, connection management, and data synchronization.

Server-Side WebSocket Manager

Located in server/src/websocket.ts, the server component creates a ws server, validates JWT-based ephemeral tokens, assigns unique socket IDs, and manages room subscriptions. It exposes critical broadcasting helpers including broadcast, broadcastToUser, and getOnlineUserIds that service layers invoke when trip data changes.

Client-Side Connection Singleton

The client/src/api/websocket.ts file implements a singleton client manager that maintains a single WebSocket connection across the application lifecycle. This module handles automatic reconnection, room joining/leaving via joinTrip and leaveTrip, and message distribution through the addListener and removeListener registration system.

Synchronization Layer

The client/src/sync/tripSyncManager.ts and client/src/sync/mutationQueue.ts files bridge WebSocket events with the UI state. This layer queues local mutations, manages optimistic updates, and triggers refetch callbacks after reconnections to ensure eventual consistency.

Authentication and Connection Handshake

TREK's WebSocket security relies on short-lived ephemeral tokens rather than long-lived JWTs in the query string.

Token Generation and Validation

Before opening a connection, the client fetches a temporary WS token via POST /api/auth/ws-token using the fetchWsToken function. The server validates this token using consumeEphemeralTokenWithMeta and loads the associated user record in setupWebSocket (lines 72-98). If validation fails due to expiration or password version changes, the server immediately closes the connection with code 4001.

Socket Identification and Rate Limiting

Upon successful authentication, the server assigns a monotonically increasing socketId (lines 106-108) and initializes a per-socket message counter. The system enforces WS_MSG_LIMIT = 30 messages per 10-second window through the socketMsgCounts map, preventing abuse while allowing normal collaborative traffic.

Room Management and Broadcasting

The architecture uses rooms—scoped channels identified by tripId—to isolate broadcast traffic and ensure updates reach only relevant participants.

Trip-Scoped Room Assignment

The server maintains a rooms map that associates each tripId with a Set<socket>. When a client sends a {type: 'join', tripId} message, the server verifies access rights via canAccessTrip before adding the socket to the appropriate room (lines 42-54). The leaveRoom function (lines 56-60) handles both explicit leave requests and automatic cleanup when sockets disconnect.

Broadcasting Strategies

Two primary broadcasting methods support different collaboration scenarios:

  • broadcast(tripId, eventType, payload, excludeSid?): Iterates through all sockets in a specific trip room, sending JSON payloads to synchronize changes like itinerary updates or todo modifications. The optional excludeSid parameter prevents echoing changes back to the originating client (lines 88-102).

  • broadcastToUser(userId, payload, excludeSid?): Walks all connected sockets to deliver user-specific notifications such as invitation acceptances or permission changes (lines 105-118).

Server-side services invoke these helpers directly after persisting data. For example, after adding an itinerary item, the service calls broadcast(tripId, 'itinerary:added', data, socketId) to push the update to all participants except the sender.

Client-Side Connection Management

The client implementation focuses on resilience, automatically recovering from network interruptions and maintaining subscription state across reconnections.

Singleton Pattern and Reconnection

The client manager in client/src/api/websocket.ts maintains a single WebSocket instance that automatically reconnects on failure. The connect function initiates the handshake, while disconnect performs graceful cleanup.

State Recovery Mechanisms

When the connection re-establishes (onopen), the client executes a preReconnectHook that flushes the pending mutation queue, ensuring local changes reach the server before fetching remote updates. Subsequently, the refetchCallback re-hydrates trip data for all activeTrips (lines 111-135). This sequence prevents synchronization conflicts and ensures the UI reflects the latest collaborative state.

Event Listening

UI components register callbacks via addListener, which stores handlers in a private set. Incoming messages are parsed and forwarded to all registered listeners, enabling decoupled components to react to specific event types like todo:update or trip:deleted.

Implementation Examples

Server: Broadcasting Trip Updates

Service layers import broadcasting utilities from the WebSocket module to push changes in real time:

// 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 originator from receiving its own update, eliminating redundant UI refreshes.

Client: Reacting to Remote Changes

Components subscribe to specific event types through the listener API:

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

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

// When a trip is opened
joinTrip(currentTripId);

Reconnection and Sync Hooks

Applications configure recovery behavior through callback registration:

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

setPreReconnectHook(() => mutationQueue.flush());      // Ensure pending writes land first
setRefetchCallback((tripId) => tripStore.refetch(tripId)); // Re-hydrate after reconnect

Summary

  • Ephemeral authentication: TREK uses short-lived WS tokens validated via consumeEphemeralTokenWithMeta to secure connections without exposing long-term credentials.
  • Room-based isolation: The rooms map in server/src/websocket.ts groups sockets by tripId, enabling efficient, scoped broadcasting through broadcast and broadcastToUser.
  • Resilient client architecture: The singleton client manager provides automatic reconnection, state recovery via preReconnectHook and refetchCallback, and a listener-based event distribution system.
  • Rate limiting: Per-socket message counters enforce WS_MSG_LIMIT = 30 messages per 10 seconds to prevent abuse.
  • Echo prevention: The excludeSid parameter in broadcast functions prevents clients from receiving their own updates.

Frequently Asked Questions

How does TREK authenticate WebSocket connections without sending JWTs in the URL?

TREK implements a token exchange pattern where the client first obtains a short-lived ephemeral WS token via POST /api/auth/ws-token. The server validates this token using consumeEphemeralTokenWithMeta during the handshake, checking expiration and user password versions. If validation fails, the connection closes immediately with code 4001, preventing unauthorized access without exposing long-lived credentials in query strings.

What happens when a client loses connection and reconnects?

The client singleton automatically re-establishes the WebSocket connection and re-joins all activeTrips stored in memory. Before requesting fresh data, it executes the preReconnectHook to flush pending mutations from mutationQueue.ts, ensuring local changes are preserved. Then it triggers the refetchCallback to re-hydrate trip data, guaranteeing that the user sees the current collaborative state without manual refresh.

How does the server prevent broadcast storms and echo effects?

TREK implements two protection mechanisms: rate limiting through socketMsgCounts (capping messages at 30 per 10 seconds per socket) and echo suppression via the excludeSid parameter in broadcast and broadcastToUser functions. When a user makes a change, the server broadcasts to all room members except the originating socket ID, preventing the sender from receiving redundant update notifications.

Can multiple users edit the same trip simultaneously without conflicts?

Yes, the architecture supports concurrent editing through the combination of optimistic updates on the client and server-side room broadcasting. When a user modifies trip data, the change is immediately broadcast to all sockets in the tripId room. Other clients receive the update via their registered listeners and merge it into local state using tripSyncManager.ts. The mutation queue ensures that pending local changes are synchronized after reconnection, maintaining eventual consistency across all 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:

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 →