CRDT Fields
CRDT Fields are experimental. The API may change between canary releases.
Two users. Same thread. Both typing at the same time.
With a normal database, somebody’s changes get silently overwritten. Locking makes the UX terrible. Server-side merging is a nightmare for character-level edits.
CRDTs fix this. A Conflict-free Replicated Data Type guarantees that any two replicas merge automatically, no matter what order edits arrive in. No central authority. No conflicts. Every peer converges on the same result.
Sp00ky uses Loro under the hood. You add a comment to your schema, and you get real-time collaborative editing with cursor presence. That’s it.
1. Annotate your schema
Add -- @crdt text above any field:
Then run spky to regenerate.
| Type | What it does |
|---|---|
text | Character-level text merging with full editor support |
SurrealDB ignores the comment. Sp00ky’s parser picks it up and wires everything for you. Today only text has built-in CrdtField wiring; for map/list/counter containers you can call field.getDoc() and drive the underlying Loro APIs directly.
2. Get the CrdtField
useCrdtField gives you a reactive CrdtField. It loads existing CRDT state, starts syncing, and rebinds when the record ID changes.
| Prop | Type | Default | Description |
|---|---|---|---|
| table | string | - | Table name |
| recordIdAccessor | () => string | undefined | - | Reactive getter for the record ID. Hook rebinds when it changes. |
| field | string | - | The CRDT-enabled field name |
| fallbackAccessor | () => string | undefined | - | Current plain text value. Seeds the editor for pre-CRDT records. |
Must live inside a <Sp00kyProvider>.
3. Pick an editor
CrdtField.getDoc() returns a standard Loro LoroDoc. Loro ships ProseMirror plugins via loro-prosemirror, so any editor built on ProseMirror works out of the box:
| Editor | Why pick it |
|---|---|
| TipTap | Most popular. Headless, extensible, great DX. |
| Milkdown | Plugin-driven Markdown editor on ProseMirror. |
| Novel | Notion-style block editor. |
| ProseMirror | Full control, no abstractions. |
Not using ProseMirror? You can still use the LoroDoc directly with any editor that supports external state. Call doc.getText('field') to read, subscribe to changes, and import/export snapshots yourself.
TipTap example
Here’s a minimal collaborative editor using TipTap + Loro. Disable TipTap’s built-in history and let LoroUndoPlugin handle undo/redo so it works correctly across peers.
For a full-featured version with cursor presence, single-line mode, and placeholder support, see CollaborativeEditor.tsx in the example app.
4. Go deeper (optional)
Building a custom editor, or wiring map/list/counter? Use the CrdtField API directly:
getDoc() gives you a standard LoroDoc. Plug in any Loro-compatible binding.
For cursor presence in a custom editor:
- Push: Call
pushCursorState(encoded)when the local cursor moves - Receive: Set
field.onCursorUpdate = (data) => { ... }to get remote cursors. The latest pending update is replayed when you attach the callback.
Helpers
Under the hood
CRDT state lives inline on the same row as the rest of your record: a @crdt-only field stores the snapshot bytes directly, and a @crdt @cursor field stores { state, cursors } so each session’s cursor blob sits next to the snapshot. There is no separate _00_crdt sidecar table.
When useCrdtField mounts, the CrdtManager ensures a single LIVE SELECT * FROM <table> is open for that table (shared across every open field on that table), imports the existing snapshot, and pushes local changes back debounced at crdtDebounceMs (default 500 ms, configurable on Sp00kyConfig). Cross-user propagation also rides the list_ref sync path so updates arrive even when SurrealDB’s cross-session LIVE-permission gap drops the parent-table notification (see Architecture).
Limitations
texthas ready-made ProseMirror integration.map/list/counterwork viagetDoc()and the Loro API directly.- SolidJS only. Flutter and Vanilla don’t have
useCrdtFieldyet. - Needs live queries. Without
LIVE SELECT, edits reconcile on reload. - API may change while experimental.