# How TREK's WebSocket Real-Time Synchronization Works Between Clients

> Discover how TREK's WebSocket real-time synchronization connects clients. Learn about authenticated connections, trip rooms, and JSON event broadcasting for seamless trip data updates.

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

---

**TREK implements a room-based WebSocket layer that synchronizes trip data across clients by authenticating connections, managing per-trip rooms, and broadcasting JSON events to all participants while handling automatic reconnection and state rehydration.**

TREK is an open-source trip management application that uses WebSocket real-time synchronization to keep multiple clients viewing the same trip updated simultaneously. The architecture relies on a room-based subscription model where authenticated sockets join per-trip rooms and receive targeted JSON events whenever data changes. This implementation, found in the `mauriceboe/TREK` repository, ensures low-latency updates while maintaining security through token-based authentication and access control checks.

## Authenticating WebSocket Connections

Before a client can participate in real-time synchronization, it must obtain a short-lived WebSocket token from the `/api/auth/ws-token` endpoint. This token includes MFA and password-version validations to ensure the session is still valid. When the client opens a WebSocket connection to `/ws?token=…`, the server extracts and verifies this token in [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts) (lines 60-78).

Upon successful verification, the server creates a **unique socket ID** for the connection and stores the authenticated user context. This ID tracks the connection throughout its lifecycle and enables targeted exclusion during broadcasts. The assignment occurs at lines 101-108 in the same file, using the ephemeral token service defined in [`server/src/services/ephemeralTokens.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/ephemeralTokens.ts).

```typescript
// Conceptual flow based on server/src/websocket.ts
const token = extractTokenFromUrl(ws.url);
const user = await verifyToken(token); // Includes MFA checks
const socketId = generateUniqueId();
socketUserMap.set(ws, { userId: user.id, socketId });

```

## Joining Trip Rooms

After authentication, the client sends a `join` message specifying the `tripId` it wants to synchronize. The server validates access permissions using the `canAccessTrip` function from [`server/src/db/database.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/db/database.ts) before admitting the socket to the room.

The room management system uses two mappings for bidirectional tracking: a **room map** that stores sets of sockets keyed by trip ID, and a **socket map** that records which rooms each socket occupies. This allows efficient cleanup when connections drop. The logic resides in [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts) at lines 42-55.

```typescript
// server/src/websocket.ts (lines 42-55)
if (msg.type === 'join' && msg.tripId) {
  const tripId = Number(msg.tripId);
  if (!canAccessTrip(tripId, user.id)) {
    ws.close(1008, 'Unauthorized');
    return;
  }
  // Add socket to room
  if (!rooms.has(tripId)) rooms.set(tripId, new Set());
  rooms.get(tripId).add(ws);
  
  // Track socket's memberships
  if (!socketRooms.has(ws)) socketRooms.set(ws, new Set());
  socketRooms.get(ws).add(tripId);
}

```

## Broadcasting Real-Time Updates

When any REST endpoint or service modifies trip data, it calls the `broadcast` function exported from [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts) (lines 90-103). This function iterates over all sockets in the specified trip room and pushes a JSON message containing the event type and payload.

The implementation includes an `excludeSid` parameter that prevents echoing updates back to the originating client, reducing unnecessary network traffic. The function only transmits to sockets with `readyState === 1` (OPEN), ensuring no errors on closed connections.

```typescript
// server/src/websocket.ts (lines 90-103)
export function broadcast(
  tripId: number | string,
  eventType: string,
  payload: Record<string, unknown>,
  excludeSid?: number | string,
): void {
  const room = rooms.get(Number(tripId));
  if (!room) return;

  const excludeNum = excludeSid ? Number(excludeSid) : null;
  for (const ws of room) {
    if (ws.readyState !== 1) continue;
    if (excludeNum && socketId.get(ws) === excludeNum) continue;
    ws.send(JSON.stringify({ type: eventType, tripId, ...payload }));
  }
}

```

## Client-Side Event Handling

On the client side, [`client/src/api/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/client/src/api/websocket.ts) manages the WebSocket singleton and event distribution. Incoming messages pass through `handleMessage` (lines 67-77), which parses the JSON and notifies all registered listeners.

Components register callbacks using `addListener` (lines 90-96) to receive specific event types such as `noteAdded` or `tripUpdated`. This decoupled approach allows multiple UI components to react to the same real-time synchronization stream without direct coupling to the transport layer.

```typescript
// client/src/api/websocket.ts
const listeners = new Set<WebSocketListener>();

export function addListener(fn: WebSocketListener): void {
  listeners.add(fn);
}

function handleMessage(event: MessageEvent) {
  const parsed = JSON.parse(event.data);
  listeners.forEach(fn => fn(parsed));
}

```

## Resilience and Reconnection Strategy

TREK's WebSocket real-time synchronization includes robust recovery mechanisms for network interruptions. When a connection drops, the client enters an exponential backoff retry loop managed by `scheduleReconnect` (lines 82-94).

Upon reconnection, the client automatically re-joins all previously active trips and executes a **refetch callback** to pull the latest canonical state from the REST API. This ensures that any missed updates during the disconnection are reconciled before the client resumes real-time synchronization. The logic spans lines 111-135 in [`client/src/api/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/client/src/api/websocket.ts).

```typescript
// client/src/api/websocket.ts (reconnection flow)
socket.onopen = () => {
  // Re-join all active trips
  activeTrips.forEach(tripId => {
    socket?.send(JSON.stringify({ type: 'join', tripId }));
  });
  
  // Rehydrate state from canonical source
  refetchCallback?.(tripId);
};

```

## Server Protection Mechanisms

To prevent resource exhaustion, the server implements **rate limiting** and **heartbeat** checks. Each socket is capped at 30 messages per 10 seconds (lines 18-30 in [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts)). Exceeding this limit results in immediate termination.

Additionally, the server sends ping frames every 30 seconds and terminates sockets that fail to respond with a pong within the expected timeframe (lines 49-57). These mechanisms ensure that stale or malicious connections do not consume server resources indefinitely.

## Summary

- **Token-based authentication**: Clients obtain short-lived WS tokens that undergo MFA and password-version validation before the socket opens at [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts).
- **Room-based architecture**: Sockets join per-trip rooms using `canAccessTrip` authorization from [`server/src/db/database.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/db/database.ts), with bidirectional tracking for efficient cleanup.
- **Targeted broadcasting**: The `broadcast` function pushes JSON events to all room participants while optionally excluding the originating socket via `excludeSid`.
- **Automatic recovery**: Clients reconnect with exponential backoff, re-join previous rooms, and refetch canonical data to synchronize missed updates.
- **Resource protection**: Rate limiting (30 msg/10s) and 30-second heartbeats prevent abuse and ensure connection health.

## Frequently Asked Questions

### How does TREK authenticate WebSocket connections securely?

TREK requires clients to obtain a short-lived token from `/api/auth/ws-token` before connecting. This token undergoes MFA and password-version checks during the handshake in [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts) (lines 60-78), ensuring only valid, active sessions can establish WebSocket connections.

### What happens when a client disconnects during a trip update?

The client implements automatic reconnection with exponential backoff via `scheduleReconnect` in [`client/src/api/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/client/src/api/websocket.ts). Once reconnected, it re-joins all active trips and executes a refetch callback (lines 111-135) to pull the latest state from the REST API, ensuring the client resynchronizes any missed updates.

### How does the server prevent broadcast storms or abuse?

The server limits each socket to 30 messages per 10 seconds (lines 18-30) and terminates connections exceeding this threshold. Additionally, the `broadcast` function accepts an `excludeSid` parameter that prevents sending updates back to the originating client, reducing redundant network traffic.

### Can the server exclude specific clients from receiving updates?

Yes. 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 provided, the function skips the socket matching that ID during iteration, allowing the server to suppress updates to the client that triggered the change or any other specific participant.