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 conflictsBecause 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:
- A ticket is requested.
- A WebSocket connects, scoped to that specific post (
docId). - 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
Full contract: API Reference → Auth → WS Ticket.
| Step | What happens |
|---|---|
| 1. Ticket | The frontend calls POST /api/ws/ticket (normal JWT auth) and gets back a one-time ticket good for 8 hours. |
| 2. Handshake | The WebSocket connects with ?ticket=<uuid>&docId=<postId>&workspaceId=<wsId> in the query string. |
| 3. Server validation | The 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. Sync | Once 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/ticketendpoint that bootstraps a session. - Post Comments & Threads — a separate feature that reuses these same live connections to push comment updates instantly.