Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/arrozet/caret/llms.txt

Use this file to discover all available pages before exploring further.

Caret’s real-time collaboration is powered by a dedicated WebSocket service that runs independently from the API Gateway. Every document open in the editor maintains a live WebSocket connection to this service, which manages a shared Y.js CRDT document in memory. Changes made by any collaborator — keystrokes, deletions, formatting — are encoded as Y.js update messages and broadcast to all other connected clients within milliseconds. Cursor positions and user presence data flow over a separate awareness channel on the same connection. The result is a seamless multiplayer editing experience similar to Google Docs, built on conflict-free replicated data types (CRDTs) that eliminate merge conflicts by design.
The collaboration service must not be proxied through the API Gateway. The frontend connects directly to wss://ws.caret.page in production (or ws://localhost:3003 locally). Routing WebSocket connections through the gateway would break the persistent connection semantics required by Y.js sync.

Connection endpoints

EnvironmentURL
Productionwss://ws.caret.page/document/{doc_id}?token={jwt}
Local developmentws://localhost:3003/document/{doc_id}?token={jwt}

Path parameters

ParameterDescription
doc_idUUID of the document to collaborate on.

Query parameters

ParameterDescription
tokenSupabase JWT for the authenticated user. Validated by the collab service on handshake.

Authentication

The JWT is passed as a ?token= query parameter rather than an HTTP header because the WebSocket handshake is a standard HTTP Upgrade request — the Authorization header is not available in browser WebSocket APIs. The collab service validates the token against Supabase during the initial handshake. If the token is missing, expired, or invalid, the server closes the connection with a 4401 or 4403 close code before any Y.js messages are exchanged.

Message protocol

All messages are binary (Uint8Array). Each message begins with a variable-length integer (varuint) that encodes the message type, followed by type-specific payload bytes encoded using the lib0 codec.

Message type 0 — Y.js sync

Sync messages carry document state for the three-phase Y.js sync handshake and subsequent incremental updates.
Sync stepDirectionDescription
Step 1Server → ClientServer sends its state vector so the client knows what the server has.
Step 2Client → ServerClient sends any updates the server is missing (based on the state vector).
Update (type 2)BidirectionalAfter initial sync, incremental CRDT updates flow in both directions as edits happen.

Message type 1 — Awareness

Awareness messages carry ephemeral presence data: who is in the document, where their cursors are, and their display information. Awareness state is not persisted — it exists only in memory and is cleared when all clients disconnect from a room. The awareness payload for each user contains:
{
  "user": {
    "user_id": "11223344-5566-7788-99aa-bbccddeeff00",
    "connected_at": 1731664200000
  }
}
The frontend augments this with display name, cursor position, and a color for the cursor decoration. The collab service broadcasts awareness updates to all other peers in the room.

Connection lifecycle

Client                          Collab Service
  │                                   │
  │── WebSocket Upgrade (HTTP) ──────►│
  │   ?token=<jwt>                    │ ← Validate JWT via Supabase
  │                                   │ ← Join room (create Y.Doc if new)
  │◄── Sync Step 1 ──────────────────│ ← Server sends state vector
  │◄── Awareness States ─────────────│ ← Server sends current presence data
  │                                   │
  │── Sync Step 2 ──────────────────►│ ← Client sends missing updates
  │◄── (optional) Update ────────────│ ← Server sends updates client was missing
  │                                   │
  │         ... bidirectional updates and awareness messages ...
  │                                   │
  │── Close ────────────────────────►│
  │                                   │ ← 30 s grace period before
  │                                   │   presence expiration broadcast
  1. Connect — Client opens a WebSocket to /document/{doc_id}?token={jwt}.
  2. Authenticate — Server validates the JWT. Invalid tokens result in an immediate close.
  3. Join room — The server joins the client to the in-memory room for doc_id, creating a fresh Y.Doc if no room exists yet.
  4. Sync step 1 — Server immediately sends its state vector so the client can compute what it needs.
  5. Awareness broadcast — Server sends the current awareness states of all connected peers.
  6. Sync step 2 — Client responds with any updates the server was missing.
  7. Steady state — Incremental CRDT updates and awareness messages flow bidirectionally in real time.
  8. Disconnect — Server waits 30 seconds before expiring the user’s presence, allowing reconnects to resume seamlessly without disrupting other collaborators.
Y.js updates are written to the document_collab_updates table when DATABASE_URL is configured in the collab service. However, room creation currently starts from a fresh Y.Doc — stored updates are not yet replayed on startup. This means that if the collab service restarts while a document is being edited, collaborators will need to reload the document to re-sync from the database copy. This is a known limitation; a future fix will call CollabPersistenceService.loadDocument before sending the initial sync step 1.

Frontend integration

The Caret frontend uses the y-websocket provider together with Tiptap’s collaboration extensions. The provider manages the connection lifecycle, reconnection back-off, and Y.js message encoding automatically.
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
import { Editor } from "@tiptap/core";
import Collaboration from "@tiptap/extension-collaboration";

const ydoc = new Y.Doc();

const provider = new WebsocketProvider(
  "wss://ws.caret.page",
  `document/${documentId}`,
  ydoc,
  {
    // Pass the Supabase JWT as a query parameter
    params: { token: supabaseJwt },
  },
);

// Listen for connection status changes
provider.on("status", ({ status }: { status: string }) => {
  console.log("Collab status:", status); // "connecting" | "connected" | "disconnected"
});

// Initialise the Tiptap editor with the shared Y.Doc
const editor = new Editor({
  extensions: [
    // ... other extensions
    Collaboration.configure({
      document: ydoc,
    }),
  ],
});

Adding user presence (cursors)

import CollaborationCursor from "@tiptap/extension-collaboration-cursor";

const editor = new Editor({
  extensions: [
    Collaboration.configure({ document: ydoc }),
    CollaborationCursor.configure({
      provider,
      user: {
        name: currentUser.displayName,
        color: "#7C3AED", // Assign a deterministic color per user
      },
    }),
  ],
});

Cleaning up

// Call when the editor unmounts or the user navigates away
function destroyCollabSession(): void {
  editor.destroy();
  provider.destroy();
  ydoc.destroy();
}

In-memory room state

The collab service keeps empty rooms alive in memory for 30 seconds after the last client disconnects (the presence expiration grace period). During this window:
  • A reconnecting client re-joins the existing room and picks up the in-memory Y.Doc state without any data loss.
  • No sync step 2 exchange is needed if the client’s local Y.Doc is still in memory.
After the grace period expires and no clients have reconnected, the room’s awareness state is cleared. The Y.Doc itself remains in memory until the service process restarts. On the next connection to that document, a fresh Y.Doc is created and synced from the client’s local state.

WebSocket close codes

CodeMeaning
1000Normal closure.
1002Protocol error — the client sent a malformed Y.js message.
1011Internal server error — check collab service logs.
4401Authentication failed — JWT missing or invalid.
4403Access denied — JWT is valid but user lacks document access.
4500Internal room error — Y.Doc could not be initialised.

Build docs developers (and LLMs) love