CRDT Overview
Cloudillo’s CRDT system uses Yrs, a Rust implementation of the Yjs CRDT (Conflict-free Replicated Data Type), to enable true collaborative editing with automatic conflict resolution. This is a separate API from RTDB, optimized specifically for concurrent editing scenarios where multiple users modify the same data simultaneously.
What are CRDTs?
Conflict-free Replicated Data Types (CRDTs) are data structures that can be replicated across multiple nodes and modified independently, then merged automatically without conflicts.
Key Properties
- Eventual Consistency: All replicas converge to the same state
- No Central Authority: No server needed to resolve conflicts
- Deterministic Merging: Same operations always produce same result
- Commutative: Order of operations doesn’t matter
- Idempotent: Applying same operation twice has no extra effect
Example: Concurrent Editing
Why Yrs?
Cloudillo uses Yrs, the Rust port of the battle-tested Yjs CRDT library:
- ✅ Production-Ready: Used in Google Docs-like applications
- ✅ Pure Rust: Memory-safe, high performance
- ✅ Rich Data Types: Text, Map, Array, XML
- ✅ Ecosystem Compatible: Works with Yjs clients (JavaScript)
- ✅ Battle-Tested Protocol: Proven sync algorithm
- ✅ Awareness API: Presence, cursors, selections
Architecture
Components
Info
The server keeps a live Doc in memory per active document, loaded from stored updates on the first connection. This lets it answer the Yjs sync handshake instantly — computing state vectors and diffs without replaying updates from disk — and merge cleanly on the last disconnect. The Doc is dropped once all clients leave.
Document Registry
Each active document holds a DocState in a global registry — the live Doc plus two broadcast channels:
The first connection loads (or initializes) the Doc and inserts the DocState; later connections share it. Each connection is tracked with a CrdtConnection that clones the channels and doc and adds its own conn_id (distinguishing multiple tabs), user_id, tn_id, throttled access/modification timestamps, and a has_modified flag. When the last client disconnects (zero receivers on both channels), the entry is removed after a grace period and the document is optimized.
Data Types
Cloudillo supports all Yjs shared types: Y.Text (collaborative text), Y.Map (key-value), Y.Array (ordered lists), and Y.XmlFragment (structured documents). See the Yjs documentation for usage details.
WebSocket Sync Protocol
Connection Flow
Message Types
All messages use the Yjs sync protocol binary format (lib0 encoding, not JSON), encoded/decoded with yrs::sync::Message:
- Sync messages carry document state:
SyncStep1(a state vector),SyncStep2(the diff of updates the peer is missing), andUpdate(a live change). - Awareness messages carry ephemeral presence/cursor state.
Sync handshake
Because the server holds a live Doc, it participates in the standard two-way y-sync handshake. On open it sends SyncStep1 with its own state vector; the client replies with SyncStep2 (and its own SyncStep1), and the server answers with a SyncStep2 diff computed from the live Doc. After the handshake, live edits flow as Update messages.
Inbound SyncStep2 data is persisted like an update, then re-encoded as an Update before broadcasting to other clients (a SyncStep2 is a handshake reply, not a live update).
Awareness Update
Presence information (cursors, selections), broadcast verbatim and never persisted.
WebSocket Connection Handler
Algorithm: Handle CRDT WebSocket Connection
Awareness (Presence)
What is Awareness?
Awareness tracks ephemeral state like:
- User presence (online/offline)
- Cursor positions
- Text selections
- Custom user state (name, color, etc.)
See the Yjs Awareness documentation for client-side integration.
Storage Strategy
Update Storage
CRDT changes are appended as individual binary updates via the CrdtAdapter. There is no separate snapshot record — updates accumulate and are merged into one compacted update during optimization.
Optimization (Update Merging)
When the last client disconnects, the server compacts the document’s stored updates into a single merged update, computed directly from the in-memory live Doc (no replay needed):
Algorithm: Optimize Document
Memory Management
When a client disconnects, the server checks whether both broadcast channels (awareness and sync) have zero receivers. If so — after the 2s grace period and a re-check — the DocState (including the live Doc) is removed from the CRDT_DOCS registry and the document is optimized. While any connection remains, the live Doc stays resident, so its memory footprint scales with the number of actively-edited documents.
Client Integration
Connect to Cloudillo’s CRDT endpoint using the standard y-websocket provider with the WebSocket URL wss://cl-o.{domain}/ws/crdt/{fileId} and an auth token as a query parameter.
Security Considerations
Authentication
WebSocket connections use axum’s OptionalAuth extractor for authentication:
- Extract auth context from the WebSocket upgrade request
- If no auth context: reject with close code 4401 (“Unauthorized”)
- Auth context provides:
id_tag,tn_id,roles, and optionalscope
Permission Enforcement
Permissions are checked once at connection time using file_access::check_file_access_with_scope(). This function evaluates:
- Scoped tokens: Share links with restricted access (read-only or read-write)
- Ownership: File owner has full access
- Tenant roles: Role-based access within the tenant
- FSHR action tokens: Federation-based file sharing permissions
The result determines whether the connection is read_only or read_write. Clients can also request a specific access level via the ?access=read or ?access=write query parameter.
Warning
Access level is checked once at connection time but not re-validated during the session. If a user’s access is revoked (e.g., an FSHR action is deleted), they retain their original access level until they reconnect.
Read-Only Enforcement
Read-only connections (determined at connection time) are enforced at the message handler level. When a read-only client sends an Update message, the server silently rejects it — the update is not stored and not broadcast. The client will see its changes rejected on the next sync cycle.
See Also
- Yjs Documentation - Official Yjs CRDT documentation
- Yrs on GitHub - Rust implementation of Yjs
- CRDT redb Implementation - How CRDT updates are persisted
- RTDB Overview - Introduction to RTDB system
- System Architecture - Overall architecture