# How TREK Implements Real-Time Collaborative Sync Using WebSockets

> Discover how TREK implements real-time collaborative sync with WebSockets. Learn about server-side rooms, event broadcasting, and automatic client reconnection for seamless shared experiences.

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

---

**TREK implements real-time collaborative sync using WebSockets by assigning authenticated clients to per-trip rooms on the server and broadcasting JSON events to all open sockets in that room, while the client automatically reconnects and re-hydrates state after dropped connections.**

The open-source `mauriceboe/TREK` project demonstrates how to build low-latency, real-time collaborative sync using WebSockets without relying on external pub/sub services. Its architecture centers on a lightweight room manager that groups authenticated sockets by trip ID and pushes granular JSON events to every participant. By coupling this with a resilient browser client that re-fetches canonical state after network interruptions, TREK keeps multi-user trip data consistent in real time.

## WebSocket Authentication and Connection Setup

Every real-time session begins with a short-lived token obtained from `/api/auth/ws-token`. The client opens a `WebSocket` to `/ws?token=…`, and the server validates the token—including **MFA** and **password-version checks**—before assigning a unique socket ID.

In [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts) (lines 60‑78), the server extracts and verifies the token. Once validated, it assigns the socket ID between lines 101‑108. This gatekeeping ensures only authorized users enter the WebSocket layer.

## Room-Based Trip Isolation

After the handshake, the client sends a `join` message containing a `tripId`. The server checks `canAccessTrip` from [`server/src/db/database.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/db/database.ts) to enforce authorization, then inserts the socket into a **room map** keyed by the numeric trip ID. Each socket also tracks its own memberships in a secondary `socketRooms` map.

According to the TREK source code in [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts) (lines 42‑55 and 49‑55), the message handler parses the `join` request and updates both maps:

```typescript
// inside websocket.ts, message handler
if (msg.type === 'join' && msg.tripId) {
  const tripId = Number(msg.tripId)
  if (!canAccessTrip(tripId, user.id)) { … }
  rooms.get(tripId)?.add(nws)          // put socket into the room
  socketRooms.get(nws)?.add(tripId)    // remember the membership
}

```

This room model isolates traffic by trip, so clients only receive updates for the trips they are currently viewing.

## Broadcasting Server Events to Clients

When a REST endpoint or service mutates trip data, it calls `broadcast(tripId, event, payload, excludeSid?)` to push the change in real time. As implemented in [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts) (lines 90‑103), the function iterates over the target room, skips closed sockets, optionally omits the originating socket, and sends a compact JSON message:

```typescript
// server/src/websocket.ts
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            // only OPEN sockets
    if (excludeNum && socketId.get(ws) === excludeNum) continue
    ws.send(JSON.stringify({ type: eventType, tripId, ...payload }))
  }
}

```

Because the broadcast happens immediately after database persistence, all connected clients stay in sync with the canonical server state.

## Client-Side Event Dispatch

On the browser side, [`client/src/api/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/client/src/api/websocket.ts) maintains a singleton WebSocket instance. Incoming messages flow through `handleMessage` (lines 67‑77), which parses the JSON payload and forwards it to every registered listener.

Clients register interest via `addListener` (lines 90‑96):

```typescript
// client/src/api/websocket.ts
export function addListener(fn: WebSocketListener): void {
  listeners.add(fn)
}

// Somewhere in a component
addListener(event => {
  if (event.type === 'joined') console.log('Joined trip', event.tripId)
})

```

This pattern decouples the transport layer from UI components, letting the frontend reactively update when `noteAdded`, `joined`, or other server events arrive.

## Resilience, Keep-Alive, and Reconnection

Real-world networks drop. TREK counters this with defensive mechanisms on both ends of the wire.

### Server Heartbeat and Rate Limiting

The Node.js server defends against stale sockets and abuse. It emits a WebSocket **ping every 30 seconds** and terminates any socket that fails to respond with a pong. Additionally, each socket is capped at **30 messages per 10 seconds**. These protections live in [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts) between lines 18‑30 (rate limit) and lines 49‑57 (heartbeat).

### Auto-Reconnect and State Re-Hydration

The client singleton in [`client/src/api/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/client/src/api/websocket.ts) manages its own retry loop via `connectInternal` and `scheduleReconnect` (lines 82‑94), using exponential back-off. Once the socket reopens, it re-joins every previously active trip and optionally invokes a **refetch callback** to pull the latest canonical data from the REST API (lines 111‑135):

```typescript
socket.onopen = () => {
  // Re‑join all trips we were watching
  activeTrips.forEach(tripId => {
    socket?.send(JSON.stringify({ type: 'join', tripId }))
  })
  // Pull fresh data from the API
  refetchCallback?.(tripId)
}

```

This dual strategy—rejoining the WebSocket room plus refetching via HTTP—guarantees that clients recover both the live event stream and any data they missed while offline.

## Putting It All Together: An End-to-End Flow

A typical real-time collaborative sync using WebSockets in TREK looks like this:

1. The client obtains a token and calls `connect()` from [`client/src/api/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/client/src/api/websocket.ts).
2. It requests trip updates with `joinTrip('42')`.
3. The server adds the socket to room `42` in [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts).
4. Another user adds a note through a REST endpoint, which triggers:

```typescript
// e.g., a note service
import { broadcast } from '@/websocket'

export function addNote(tripId, note) {
  // …persist note in DB…
  broadcast(tripId, 'noteAdded', { note }, socketId) // push to all sockets
}

```

5. Every open client in room `42` receives the event, `handleMessage` parses it, and registered listeners update the UI instantly.

## Summary

- **TREK** implements real-time collaborative WebSocket sync through a **room-based architecture** where each trip ID maps to an isolated set of sockets.
- **Authentication** uses short-lived tokens validated at handshake time, including MFA and password-version checks in [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts).
- **Broadcasting** is handled by a lightweight `broadcast()` function that pushes JSON only to open sockets in the target room, with optional sender exclusion.
- **Client isolation** is enforced by `canAccessTrip` before any socket enters a room, keeping trip data private.
- **Resilience** is built in via server heartbeats, rate limiting, exponential back-off reconnect, and REST re-hydration callbacks.

## Frequently Asked Questions

### How does TREK authenticate WebSocket connections?

TREK issues a short-lived token from `/api/auth/ws-token` that the client passes when opening `/ws?token=…`. The server verifies this token in [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts) (lines 60‑78), performs MFA and password-version checks, and then assigns a unique socket ID before allowing further messages.

### What prevents one client from receiving updates for another user's trip?

The server calls `canAccessTrip` inside the `join` message handler in [`server/src/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/websocket.ts) (lines 42‑55). If the user is not authorized for the requested `tripId`, the socket is rejected from the room. Broadcasts are scoped solely to sockets inside that trip's room, so data never leaks across trips.

### How does TREK handle a client that loses network connectivity?

The client manager in [`client/src/api/websocket.ts`](https://github.com/mauriceboe/TREK/blob/main/client/src/api/websocket.ts) detects closures and schedules reconnections with exponential back-off via `scheduleReconnect` (lines 82‑94). Once the connection restores, the client automatically re-joins all active trips and runs a refetch callback (lines 111‑135) to merge any missed REST state back into the UI.

### Why does TREK use both WebSocket broadcasts and REST refetching?

WebSocket broadcasts deliver low-latency, real-time updates to active clients, but a disconnected client can miss events. The optional refetch callback triggered after reconnect pulls the full, canonical state over HTTP, ensuring the client eventually converges to the correct data even after prolonged outages.