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

> Discover how TREK leverages WebSockets for real-time collaboration and room-based trip channels. Learn about authentication, room grouping, and state recovery for seamless user experiences.

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

---

**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`](https://github.com/mauriceboe/TREK/blob/main/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`](https://github.com/mauriceboe/TREK/blob/main/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`](https://github.com/mauriceboe/TREK/blob/main/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`](https://github.com/mauriceboe/TREK/blob/main/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`](https://github.com/mauriceboe/TREK/blob/main/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:

```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 update from echoing back to the originating client, avoiding redundant work.

### Client: Handling Real-Time Events

Components subscribe to events through the listener API:

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

```typescript
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`](https://github.com/mauriceboe/TREK/blob/main/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`](https://github.com/mauriceboe/TREK/blob/main/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`](https://github.com/mauriceboe/TREK/blob/main/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.