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

> Explore how TREK utilizes real-time WebSocket collaboration with JWT authentication and room-based architecture. Learn about synchronized trip data, ephemeral tokens, and broadcasting.

- Repository: [Maurice/TREK](https://github.com/mauriceboe/TREK)
- Tags: deep-dive
- Published: 2026-07-04

---

**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`](https://github.com/mauriceboe/TREK/blob/main/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`](https://github.com/mauriceboe/TREK/blob/main/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`](https://github.com/mauriceboe/TREK/blob/main/client/src/sync/tripSyncManager.ts) and [`client/src/sync/mutationQueue.ts`](https://github.com/mauriceboe/TREK/blob/main/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`](https://github.com/mauriceboe/TREK/blob/main/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:

```typescript
// 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:

```typescript
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:

```typescript
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`](https://github.com/mauriceboe/TREK/blob/main/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`](https://github.com/mauriceboe/TREK/blob/main/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`](https://github.com/mauriceboe/TREK/blob/main/tripSyncManager.ts). The mutation queue ensures that pending local changes are synchronized after reconnection, maintaining eventual consistency across all participants.