Collaborative editing in DeepSpace is backed by Yjs, a CRDT library. The SDK ships three hooks for binding Yjs documents to React state.Documentation Index
Fetch the complete documentation index at: https://docs.deep.space/llms.txt
Use this file to discover all available pages before exploring further.
useYjsText- collaborative plain text bound to a record field.useYjsField- rawY.Docaccess for structured types (maps, arrays, XML fragments) on a record field.useYjsRoom- a standalone Yjs document not tied to any record.
useYjsText and useYjsField sync over the record’s existing RecordRoom connection. useYjsRoom opens a dedicated YjsRoom Durable Object per docId. See the real-time reference for full signatures.
Record-bound hooks (
useYjsText, useYjsField) require a <RecordProvider> ancestor and a registered collection schema with an existing record. The scaffold from npm create deepspace sets up the provider. useYjsRoom has neither requirement and connects directly to the YjsRoom DO.Collaborative text
Bind a<textarea> to a Yjs Y.Text on a record. Two clients editing the same (collection, recordId, fieldName) see each other’s keystrokes live.
synced is true so the user doesn’t type into the document before initial state arrives. canWrite mirrors the collection’s RBAC - see Permissions.
Structured data
For lists, maps, or any non-text Yjs type, useuseYjsField. It returns the raw Y.Doc plus an updateCount that ticks on every local or remote update - use it as a render trigger.
Y.Doc, not a value. Read whichever Yjs type you need off doc (doc.getArray, doc.getMap, doc.getXmlFragment) and rely on updateCount or a Yjs observe to drive React re-renders. For simple text, prefer useYjsText.
Standalone rooms
UseuseYjsRoom for documents not tied to a record: scratchpads, whiteboards, ephemeral sessions. The first argument is any string docId; two clients passing the same docId connect to the same YjsRoom Durable Object.
Y.Text field exposed as text / setText on the hook return - useful when the room is mostly a text doc. For non-text data, ignore those fields and reach into doc directly as shown above.
YjsRoom authentication and roles
The/ws/yjs/:docId route in the scaffold is the only /ws/* route that is token-required and docs-aware. The other /ws/* routes use the inline wsRoute helper, which allows anonymous connections (see Security model). The Yjs handler instead:
- Returns
401if no verified JWT is present on the upgrade. - Looks up
documents[docId](the docs feature’s collection) and resolves a Yjs role from the row:adminwhen the caller isdocuments.ownerIdor the app’sOWNER_USER_ID.memberwhen the caller’s id is indocuments.editors(JSON array stored as text).viewerwhen the caller’s id is indocuments.collaborators(JSON array stored as text).403otherwise.
- When the app has no
documentscollection registered (or the row is not found), the handler falls through to'member'for any authenticated user, so apps that useuseYjsRoomwithout the docs feature still work.
role is appended to the DO URL as a query parameter; the DO uses it to set canWrite (admins and members write, viewers are read-only).
Editor integrations
The Yjs ecosystem has bindings for popular editors. The SDK doesn’t ship them, but the hooks expose the underlyingY.Doc and awareness, which is everything these bindings need.
- Tiptap - rich-text editor with Yjs collaboration.
- ProseMirror - the
y-prosemirrorbinding. - Monaco -
y-monacofor code editors.
@tiptap/extension-collaboration-cursor alongside Tiptap if you want peer cursors; pass awareness from the hook through its provider option.
Cursors and selections
Yjs’s awareness protocol broadcasts ephemeral peer state - cursors, selections, names. BothuseYjsField and useYjsRoom expose an awareness instance. Pass it to an editor binding (like @tiptap/extension-collaboration-cursor shown above), or read and write it directly when building a custom UI.
The awareness instance returned from useYjsRoom is already wired to the same WebSocket: any call to awareness.setLocalState or awareness.setLocalStateField triggers an MSG_AWARENESS frame to peers, and incoming peer frames update the map you read with awareness.getStates().
cursor, selection, user ({ name, color }), and typing. Anything you put under a key with setLocalStateField shows up in every peer’s getStates() map until you clear it or disconnect. The docs feature’s Tiptap toolbar wires the same awareness instance into @tiptap/extension-collaboration-cursor, which handles the cursor/selection encoding for you - see Editor integrations above.
For the raw protocol primitives (MSG_AWARENESS, encodeAwarenessMessage, handleAwarenessMessage), see low-level sync primitives in the reference. For non-Yjs presence (live cursors on a canvas, who’s typing in a chat), use usePresenceRoom instead - a separate, lighter-weight room type.
When to use Yjs vs records
DeepSpace gives you two ways to share data. Pick based on what you’re modeling.| Use records when… | Use Yjs when… |
|---|---|
| Each row is a discrete entity (a todo, a message, a user) | The data is a single document being co-edited |
| Fields are queryable and filterable | The data has interactive merging needs |
| RBAC needs to filter visibility | All editors should see the same content |
| Updates are coarse-grained | Updates are character-by-character |
Persistence
Record-bound Yjs fields (useYjsText, useYjsField) persist inside the record’s RecordRoom. Standalone documents (useYjsRoom) persist inside a YjsRoom DO keyed by docId. In both cases the document is stored as a single SQLite blob that’s re-encoded from the current Y.Doc state on every update, so the blob grows with the document’s current size, not with edit history. New clients receive the full state on connection, and documents survive worker redeployments.
Next steps
- Real-time reference - full hook signatures and return shapes.
- Canvas - collaborative shapes and viewports via
useCanvas. - Presence and cursors - live cursors and typing indicators.
- Real-time sync - how sync works under the hood.