# How TREK Handles Real-Time Collaboration via WebSockets: Architecture & Implementation

> Discover how TREK implements real-time collaboration using WebSockets. Learn about its architecture, JWT authentication, ephemeral tokens, and state recovery for seamless synchronization.

- Repository: [Maurice/TREK](https://github.com/mauriceboe/TREK)
- Tags: architecture
- Published: 2026-07-03

---

**TREK implements real-time collaboration via a lightweight, JWT-authenticated WebSocket layer that synchronizes trip data across clients using ephemeral tokens, trip-scoped rooms, and automatic reconnection with state recovery.**

The open-source TREK application enables multiple users to simultaneously view and edit trip itineraries through a robust WebSocket architecture. This article examines the implementation details from the server-side socket management to the client-side synchronization layer, referencing actual source files from the `mauriceboe/TREK` repository.

## Authentication and Handshake Flow

TREK uses a **two-phase authentication** process to establish secure WebSocket connections without exposing long-lived credentials.

### Ephemeral Token Generation

Before opening a socket, the client requests a short-lived WS token via `POST /api/auth/ws-token`. In [`client/src/api/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/client/src/api/websocket.ts), the `fetchWsToken` function retrieves this token, which typically expires within minutes.

### Server-Side Validation

The server validates the ephemeral token using `consumeEphemeralTokenWithMeta` within the `setupWebSocket` function in [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts) (lines 72-98). If validation fails—whether due to expiration, invalid signatures, or password version mismatches—the server immediately terminates the connection:

```typescript
// server/src/websocket.ts (simplified)
if (!isValidToken) {
  socket.close(4001, 'Authentication failed');
  return;
}

```

Upon successful authentication, the server assigns a monotonically increasing `socketId` to each connection (lines 106-108) and sends a welcome message containing the identifier.

## Socket Identification and Rate Limiting

To prevent abuse, TREK enforces **per-socket rate limiting** through a message counter system. The `socketMsgCounts` map tracks each socket's activity, enforcing a hard limit of `WS_MSG_LIMIT = 30` messages per 10-second window. If a client exceeds this threshold, the server can throttle or disconnect the offending socket.

## Room Management for Trip-Scoped Channels

TREK organizes WebSocket connections into **rooms** based on `tripId`, enabling targeted broadcasting to only relevant participants.

### Room Structure

The server maintains a `rooms` Map that associates each `tripId` with a `Set<socket>` containing all active connections for that trip. This structure lives in [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts) and enables O(1) lookup for broadcast operations.

### Joining and Leaving Rooms

When a user opens a trip in the UI, the client calls `joinTrip(tripId)`, which sends a message with `{type: 'join', tripId}`. The server verifies access rights via `canAccessTrip` before adding the socket to the appropriate room (lines 42-54):

```typescript
// Server-side room management
if (await canAccessTrip(userId, tripId)) {
  rooms[tripId].add(socket);
}

```

When users navigate away or close the application, `leaveTrip` triggers the `leaveRoom` function (lines 56-60), automatically cleaning up the socket from the room set. The server also calls this cleanup in the socket close handler (lines 62-70) to prevent memory leaks.

## Broadcasting Mechanisms

TREK provides two distinct broadcasting patterns to support different collaboration scenarios.

### Trip-Wide Broadcasts

The `broadcast(tripId, eventType, payload, excludeSid?)` function iterates through all sockets in a specific trip room, sending JSON payloads to every participant. The optional `excludeSid` parameter prevents echo-back to the originating client, avoiding redundant UI updates:

```typescript
// src/services/tripService.ts
import { broadcast } from '../../src/websocket';

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

```

This helper is defined in [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts) (lines 88-102) and used throughout the server codebase whenever trip data mutations occur.

### User-Wide Broadcasts

For cross-trip notifications or user-specific updates, `broadcastToUser(userId, payload, excludeSid?)` walks all connected sockets, filtering for those belonging to the specific user (lines 105-118). This enables features like notifying a user across all their devices when their profile updates.

## Client-Side Connection Management

The client implementation in [`client/src/api/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/client/src/api/websocket.ts) uses a **singleton pattern** to maintain a single WebSocket connection across the application lifecycle.

### Connection Lifecycle

The `connect()` function establishes the socket using the ephemeral token, while `disconnect()` performs graceful cleanup. The manager exposes `addListener` and `removeListener` methods that allow UI components to subscribe to specific event types without managing raw WebSocket objects:

```typescript
// Client-side listener registration
import { addListener, joinTrip } from '@/api/websocket';
import { useTripStore } from '@/store/trip';

addListener((msg) => {
  if (msg.type === 'itinerary:added' && msg.tripId) {
    useTripStore().applyRemoteAdd(msg.tripId, msg.itemId, msg.data);
  }
});

joinTrip(currentTripId);

```

### Automatic Reconnection

The client handles network interruptions through an exponential backoff reconnection strategy. When the connection drops, the manager automatically attempts to reconnect and restores the previous session state.

## State Recovery and Synchronization

TREK ensures data consistency across reconnections through a sophisticated **sync layer** that bridges the WebSocket API with the client-side store.

### Pre-Reconnection Hooks

Before reconnecting, the client executes a `preReconnectHook` to flush any pending mutations from the `mutationQueue`. This ensures that local changes are persisted to the server before fetching fresh data:

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

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

```

### Trip Rejoining

Upon successful reconnection (`onopen` event), the client automatically rejoins all `activeTrips` and triggers `refetchCallback` to rehydrate the UI with the latest server state (lines 111-135 in [`client/src/api/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/client/src/api/websocket.ts)). This mechanism prevents synchronization gaps that could occur during temporary network losses.

## End-to-End Collaboration Flow

1. **User opens the application**: `connect()` initiates the handshake, fetching an ephemeral token and establishing the WebSocket.
2. **Server authenticates**: Validates the token, assigns a `socketId`, and sends a welcome message.
3. **User views a trip**: Client calls `joinTrip(tripId)`, server verifies access via `canAccessTrip`, and adds the socket to the room.
4. **Collaborative edit occurs**: When any user modifies trip data, the server calls `broadcast(tripId, eventType, payload)`.
5. **Real-time updates**: All clients in the room receive the event, and listeners in [`client/src/sync/tripSyncManager.ts`](https://github.com/mauriceboe/TREK/blob/main/client/src/sync/tripSyncManager.ts) update local state, instantly reflecting changes across all connected devices.

## Summary

- **Ephemeral authentication**: TREK uses short-lived WS tokens fetched via `POST /api/auth/ws-token` and validated in [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts) to secure connections without exposing long-lived JWTs.
- **Trip-scoped rooms**: The server organizes sockets into rooms by `tripId`, enabling efficient targeted broadcasting via `broadcast()` and `broadcastToUser()`.
- **Rate limiting**: Each socket is limited to 30 messages per 10 seconds through `socketMsgCounts` to prevent abuse.
- **Client singleton**: The [`client/src/api/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/client/src/api/websocket.ts) singleton manages connection state, automatic rejoining of active trips, and listener registration.
- **State recovery**: The sync layer in [`client/src/sync/tripSyncManager.ts`](https://github.com/mauriceboe/TREK/blob/main/client/src/sync/tripSyncManager.ts) ensures data consistency across reconnections by flushing mutation queues and refetching trip data.

## Frequently Asked Questions

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

TREK uses ephemeral tokens obtained via `POST /api/auth/ws-token` before connection establishment. The client passes this short-lived token as a query parameter (`ws://…/ws?token=…`), which the server validates using `consumeEphemeralTokenWithMeta` in [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts). Once validated, the server associates the socket with the user ID and discards the ephemeral token, preventing credential exposure in server logs or browser history.

### What happens when a user loses connection during a collaborative editing session?

The client automatically attempts to reconnect using exponential backoff. Upon reconnection, the `preReconnectHook` flushes any pending mutations from the `mutationQueue`, and the client rejoins all previously active trips. If a `refetchCallback` is configured, the client fetches fresh data to ensure consistency with any changes made by other users during the disconnection period.

### How does TREK prevent a user from receiving their own updates twice?

The `broadcast` function in [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts) accepts an optional `excludeSid` parameter. When a user triggers a mutation (like adding an itinerary item), the server calls `broadcast(tripId, 'itinerary:added', data, socketId)`, passing the originating socket's ID. The broadcast iteration skips this socket ID, preventing echo-back while ensuring all other participants receive the update.

### What is the maximum number of messages a client can send through the WebSocket?

TREK enforces a rate limit of **30 messages per 10 seconds** per socket through the `socketMsgCounts` tracking mechanism in [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts). This limit prevents spam and ensures server resources remain available for legitimate collaborative operations.