Features
Real-Time Collaboration

Real-Time Collaboration

Open the same post as a teammate and watch each other type. Real-time collaboration merges everyone's edits automatically — no locking, no "this post is being edited by someone else" wall, no conflicting-versions dialog. Under the hood it's Yjs (opens in a new tab) (a CRDT) synced over a dedicated WebSocket connection, completely separate from the app's normal REST API.

This page covers the user-facing behavior. For the wire-level protocol, DynamoDB schemas, and every edge case, see the authoritative spec: specs/features/collab.md. For the POST /api/ws/ticket HTTP contract specifically, see API Reference → Auth.

What you get

  • Live text sync. Every keystroke from every collaborator merges into the shared document automatically, character-level, with no "save" step and no conflicts to resolve.
  • Colored cursors. Each collaborator gets a colored cursor and text selection highlight with their name attached, rendered directly in the canvas editor.
  • A synced badge. The editor knows whether it has completed its initial sync with the server — useful to avoid flashing an empty document before the real content has arrived.
  • Resilient reconnects. Every edit is persisted server-side as it happens. Close the tab, come back an hour later, and the full document replays from storage — nothing is ever only "in the socket."
⚠️

There is currently no UI for a "who else is online" presence list beyond the cursors themselves rendering while someone is actively in the document — see Edge cases.

How it works, at a glance

 open post in canvas editor


 POST /api/ws/ticket  ──▶  { "ticket": "<uuid4>" }


 wss://<ws-domain>/<stage>/?ticket=...&docId=<postId>&workspaceId=<wsId>


 $connect: validate ticket + workspace membership, register connection


 client sends "syncStep1"  ──▶  server replies with full stored document


 you type  ──▶  Yjs generates a binary update
        │            │
        │            ▼
        │       sent over the socket, appended to storage,
        │       broadcast to every other open connection on this post

 every collaborator's editor applies the same update — no conflicts

Because browsers can't attach an Authorization header to a WebSocket handshake the way a normal fetch can, connecting is a two-step dance: get a short-lived ticket over the regular authenticated REST API, then hand that ticket to the WebSocket as a query parameter. See Connecting below.

Where it lives

The collaboration session starts automatically whenever you open a post in the canvas editor (useEditor hook) — there's no separate button or toggle. As soon as the editor mounts:

  1. A ticket is requested.
  2. A WebSocket connects, scoped to that specific post (docId).
  3. The TipTap editor gains its collaboration extensions once the connection is ready, and your cursor starts broadcasting position/selection to everyone else in the document.

Leaving the page (navigating away, closing the tab) tears the connection down cleanly.

Connecting

StepWhat happens
1. TicketThe frontend calls POST /api/ws/ticket (normal JWT auth) and gets back a one-time ticket good for 8 hours.
2. HandshakeThe WebSocket connects with ?ticket=<uuid>&docId=<postId>&workspaceId=<wsId> in the query string.
3. Server validationThe server checks the ticket is valid and unexpired, and that you're a member of the workspace the post belongs to. Any workspace role (member, admin, owner) is sufficient — there's no editor-only restriction here.
4. SyncOnce connected, the client requests the document's current state and the server sends it back as a Yjs update; live edits flow both ways from then on.

A rejected ticket or failed membership check closes the connection before it ever opens — the editor simply never gains its collaboration extensions (you'd still see the document in read-only-ish local state, without live sync).

Presence and cursors

Everyone with the same post open sees:

  • A colored cursor at each collaborator's current position.
  • A highlighted selection when a collaborator has text selected.
  • Their name attached to their cursor.

Colors are assigned deterministically per session (hashed from part of your auth token), not from a stable per-user color you'd recognize across devices — the same person can show up in a different color if they reconnect under different session state. There are 10 possible colors, so on a large enough workspace two people can occasionally match.

Staying in sync while idle

The browser tab keeps a lightweight heartbeat going to the server so the connection's server-side bookkeeping doesn't expire while you're still looking at the page (WebSocket connections that go fully silent for too long are cleaned up automatically). You don't need to do anything — this is handled transparently by the editor.

Edge cases worth knowing

  • No dedicated "who's online" panel. The only visible presence signal today is the cursors themselves — if nobody else has an active cursor visible, you can't otherwise tell who has the post open.
  • Any workspace member can join any post's session in that workspace — collaboration isn't restricted to people explicitly assigned to the post.
  • A ticket isn't strictly single-use. The same minted ticket can authenticate more than one connection attempt (e.g. a page reload) until it naturally expires 8 hours after issue.
  • Very long-lived, heavily-edited posts can accumulate a large edit history server-side faster than it's currently trimmed — see the Known limitation below. This doesn't affect correctness (every edit still replays), only the size of what a new connection has to load.

Compaction

The system is designed to periodically compact a document's stored edit history into a single snapshot once it's accumulated more than 100 incremental edits, to keep new-connection load times fast. As currently wired, this scheduled job is not actually reaching the compaction code path — see specs/features/collab.md#compaction for the exact mechanics. In practice this only matters for posts with an unusually long, heavily-edited history; typical editing sessions are unaffected.

Related pages

  • specs/features/collab.md — the full technical spec: connection lifecycle, DynamoDB schemas, the Yjs sync protocol, and every documented edge case.
  • API Reference → Auth — the POST /api/ws/ticket endpoint that bootstraps a session.
  • Post Comments & Threads — a separate feature that reuses these same live connections to push comment updates instantly.