Documents many people edit at the same time with live cursors, offline edits and history: Yjs documents synced and stored in your project, access by row level security.
Collaborative documents let several people edit the same thing at once — a text document, a whiteboard, a form, a spreadsheet — and see each other's changes and cursors live. Your app keeps the document in a Yjs Y.Doc, which any Yjs editor binding can use: TipTap, Lexical, ProseMirror, CodeMirror, Monaco, BlockNote, Quill and others. StitchDB syncs the document between everyone who has it open, stores it in your project database and keeps its history.
- Live: changes reach the others in the document as they type, over one WebSocket for every document the app has open.
- Offline: edits made without a connection are kept and sent when the app is back online. Edits from several people merge without conflicts.
- Access by row level security: who may open and edit a document is decided by policies on
collab.documents, like any other table. - History: a snapshot is saved every hour a document changes, and your app can take named ones.
#Turn it on
In Database › Collaborative docs (project owners and admins) choose Turn on collaborative docs. This adds the collab schema to your database:
| Table | What it holds |
|---|---|
collab.documents | One row per document: id (your name for it, up to 200 characters), owner_id, org_id, title, metadata, size and when it was last edited. Row level security on this table decides access. |
collab.members | Who a document is shared with: doc_id, user_id and role (viewer or editor). |
collab.updates, collab.snapshots | The stored document and its history. Not readable through the API; use the SDK or Studio. |
#Open a document
npm install stitchdb-js yjsimport * as Y from 'yjs'
import { createClient } from 'stitchdb-js'
const stitchdb = createClient('https://<ref>.p.stitchdb.com', '<anon key>')
const collab = stitchdb.collab({ Y }) // pass your yjs module
const ydoc = new Y.Doc()
const room = await collab.open('plan-2026', ydoc, { create: { title: 'Plan 2026' } })
room.write // true if this user may edit
ydoc.getText('body').insert(0, 'Hello') // sent to everyone in the documentopen() resolves once the stored document is loaded into ydoc. With create, a document that doesn't exist yet is created, owned by the signed-in user. Without it, opening an unknown document fails with code: 'forbidden', the same as one the user may not open, so ids can't be guessed.
Bind ydoc to your editor the way its Yjs binding describes, for example with TipTap:
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import Collaboration from '@tiptap/extension-collaboration'
new Editor({ extensions: [StarterKit.configure({ history: false }), Collaboration.configure({ document: ydoc })] })#Cursors and names
Presence is for things that should not be saved: cursors, selections, names, colours. Each person sets their own state; the others get it live and it disappears when they leave.
room.setPresence({ name: user.name, color: '#0e9f6e', cursor: 42 })
room.on('presence', (peers) => {
for (const [client, state] of peers) drawCursor(client, state)
})
room.setPresence(null) // hide yourself#Who can open and edit
A document the user can select in collab.documents they can open; one they can update they can edit. Viewers get the document and live changes, but their own edits stay on their device (room.write is false). The checks run as the user, with the same claims as the REST API, when they open a document, when their token is refreshed and every few minutes while it stays open. Someone who loses access is removed from the document.
Turning collaborative docs on adds these starting policies:
| Who | Can |
|---|---|
| Any signed-in user | Create a document; they become its owner |
| The owner | Open, edit, delete and share it |
| People it is shared with | Open it; edit it if their role is editor |
Share a document from your app (by default only its owner may) — this adds a row to collab.members:
await collab.share('plan-2026', bobId, 'editor') // or 'viewer'; again to change the role
await collab.members('plan-2026') // [{ user_id, role, created_at }]
await collab.unshare('plan-2026', bobId)
await collab.list() // documents this user can open, with their accessOr change the rules entirely. Policies can use the helper collab.access(doc_id), which returns 'owner', 'editor', 'viewer' or null for the signed-in user. For example, to let everyone in the document's organization edit it:
create policy "Org members can open" on collab.documents
for select to authenticated using (auth.is_member(org_id));
create policy "Org members can edit" on collab.documents
for update to authenticated using (auth.is_member(org_id)) with check (auth.is_member(org_id));#History
const id = await room.snapshot('Before review') // a named snapshot
const versions = await room.snapshots() // newest first: { id, label, auto, created_at, size_bytes }
const old = await room.loadSnapshot(versions[1].id) // a separate Y.Doc with the content at that moment
// restore: copy the old content into the live document as a new change
const text = ydoc.getText('body')
ydoc.transact(() => { text.delete(0, text.length); text.insert(0, old.getText('body').toString()) })A snapshot is saved automatically every hour a document changes. Snapshots older than your plan's history are deleted, except the newest one of each document. In Studio, Database › Collaborative docs lists documents; open one to see who it is shared with, its snapshots and recent editors, take a snapshot or delete it.
#Offline and reconnecting
When the connection drops, room keeps working on the device: edits are kept (room.unsaved counts them) and the SDK reconnects by itself, gets only what changed while it was away, and sends the kept edits. room.on('status', (s) => …) reports connecting, connected and offline. To keep edits across a page reload too, add a local Yjs store such as y-indexeddb to the same ydoc.
The stored document grows with every change. When it has many stored changes, the next editor who opens it replaces them with one compact copy (compactAfter, default 200), so opening stays fast.
#From your server
Read or write a document over HTTPS, for example to import content, export it or generate a first draft. Send the project key in apikey and the user's token, or the service key, as a bearer token. Document content is Yjs updates, base64.
| Request | Does |
|---|---|
GET /collab/v1/docs/<id> | The document: { seq, updates: [base64] }; apply each update to a Y.Doc |
POST /collab/v1/docs/<id>/updates | { data }: add a Yjs update; everyone with the document open gets it live |
GET /collab/v1/docs/<id>/snapshots | The history, newest first |
GET /collab/v1/docs/<id>/snapshots/<n> | One snapshot: { updates: [base64] } |
POST /collab/v1/docs/<id>/snapshots | { label }: take a snapshot |
GET /collab/v1/docs | The documents the caller can open |
GET, POST /collab/v1/docs/<id>/members | Who it is shared with; { user_id, role } to share |
#Errors
| code | Meaning |
|---|---|
forbidden | The document doesn't exist, or this user may not open it (or edit it, for an edit) |
too_large | The document is over your plan's size limit. Open it with a writer to compact it, or start a new one |
rate_limited | More than about 30 changes a second on one connection; the SDK sends them again a moment later |
too_many_connections | Your project has the plan's number of editors connected at once |
#Limits
| Limit | Free | Pro | Scale | Enterprise |
|---|---|---|---|---|
| Collaborative document size | 2.0 MB | 10 MB | 25 MB | 50 MB |
| Collaborative editing connections at once per project | 50 | 500 | 2,000 | 10,000 |
| Collaborative document history | 1 days | 7 days | 30 days | 90 days |
One connection can have up to 50 documents open. A single change can be up to 2 MB; presence state up to 8 KB.