How AppFlowy Synchronizes Real-Time Collaborative Features Using CRDTs and WebSockets
AppFlowy synchronizes real-time collaborative features through a CRDT-based sync stack that connects local Collab objects to AppFlowy Cloud via WebSockets, using a configurable builder pattern to conditionally attach SyncPlugins when live collaboration is enabled.
The open-source AppFlowy-IO/AppFlowy repository implements real-time collaboration by layering a plugin-based synchronization system on top of conflict-free replicated data types (CRDTs). This architecture allows document managers to enable or disable live syncing per object while abstracting network concerns away from the UI layer.
The CRDT-Based Sync Architecture
AppFlowy’s real-time collaboration relies on a Collab object model backed by the y-crdt library. The sync infrastructure is divided into four distinct layers that handle configuration, plugin injection, transport, and persistence.
Collab Builder and Configuration
Every collaborative object (documents, folders, user awareness) originates from the CollabBuilder in frontend/rust-lib/collab-integrate/src/collab_builder.rs. The builder consults CollabBuilderConfig::sync_enable (default false) to decide whether the object should participate in real-time collaboration.
// Lines 401-412 in collab_builder.rs
let config = CollabBuilderConfig::default().sync_enable(true);
let collab = collab_builder
.build_collab(workspace_id, uid, object_id, CollabType::Document, config)
.await?;
When sync_enable is set to true (typically by document managers such as flowy-document/src/manager.rs at line 214), the builder prepares the object for network synchronization.
SyncPlugin and WebSocket Transport
If synchronization is enabled, the builder attaches a SyncPlugin to the Collab instance. This plugin opens a WebSocketChannel<ServerCollabMessage> via the client_api::collab_sync crate, streaming local CRDT operations to the server and applying inbound updates.
// Line 331 in collab_builder.rs
if build_config.sync_enable {
let sync_plugin = SyncPlugin::new(...);
collab_builder.with_plugin(Box::new(sync_plugin));
}
The SyncPlugin is imported in frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs (line 2) and encapsulates all networking logic, ensuring the UI remains agnostic of connection state.
Cloud Service Provider Abstraction
The CollabCloudPluginProvider trait in frontend/rust-lib/collab-integrate/src/plugin_provider.rs (lines 10-26) determines which plugins load based on the execution environment. It implements is_sync_enabled to return the configuration flag, allowing the same codebase to operate in offline (no plugin) or cloud modes.
// From plugin_provider.rs and cloud_service_impl.rs
impl CollabCloudPluginProvider for MyProvider {
fn is_sync_enabled(&self) -> bool { self.sync_enabled }
fn get_plugins(&self, ctx: CollabPluginProviderContext) -> Vec<Box<dyn CollabPlugin>> {
if ctx.is_cloud() && self.is_sync_enabled() {
vec![Box::new(SyncPlugin::new(...))]
} else {
vec![] // NoSyncPlugin implicit via empty vec
}
}
}
How the Synchronization Flow Works
The end-to-end real-time collaboration flow follows six distinct steps:
- Object Creation: A manager (e.g., document, folder, or user-awareness manager) creates a
Collabinstance withsync_enable(true)via the builder. - Plugin Injection: The builder detects the sync flag and attaches the
SyncPlugin, which initializes a WebSocket connection to AppFlowy Cloud. - Local Edit Transformation: User edits are transformed by
y-crdtinto compact operations (ops). - Upstream Transmission: The
SyncPluginsends these ops asServerCollabMessagestructs over the WebSocket. - Server Processing: The cloud service receives messages, persists them to the shared CollabKVDB (a KV CRDT store), and broadcasts to subscribed clients.
- Downstream Application: Remote clients receive ops, apply them to local CRDT instances, and update the UI instantly.
If sync_enable is false or the network is unavailable, the Collab operates purely locally with no plugin attached.
Key Source Files and Implementation Details
| File Path | Responsibility |
|---|---|
frontend/rust-lib/collab-integrate/src/collab_builder.rs |
Builds Collab instances and conditionally injects SyncPlugin at line 331 based on sync_enable configuration (lines 401-412). |
frontend/rust-lib/collab-integrate/src/plugin_provider.rs |
Defines the CollabCloudPluginProvider trait (lines 10-26) that selects cloud sync or local-only plugins. |
frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs |
Concrete implementation supplying the SyncPlugin via client_api::collab_sync imports (line 2). |
frontend/rust-lib/flowy-server/src/server.rs |
WebSocket server creating Arc<WebSocketChannel<ServerCollabMessage>> for each client at line 142. |
frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs |
Manages user-awareness CRDT with sync enabled; references CollabKVDB at line 335. |
frontend/rust-lib/flowy-document/src/manager.rs |
Enables sync for documents at line 214 via CollabBuilderConfig::sync_enable. |
frontend/rust-lib/flowy-folder/src/manager.rs |
Enables sync for folder hierarchies. |
frontend/rust-lib/flowy-database2/src/manager.rs |
Enables sync for database objects. |
Code Examples
Enabling Sync for Documents and Objects
Document managers explicitly opt into real-time collaboration by setting the sync flag before building the Collab object:
// flowy-document/src/manager.rs (line 214)
let config = CollabBuilderConfig::default().sync_enable(sync_enable);
let collab = self
.collab_builder
.await?
.build_collab(
workspace_id,
uid,
object_id,
CollabType::Document,
config,
)
.await?;
Attaching the SyncPlugin in the Builder
The builder logic branches at line 331 to attach the networking plugin only when configured:
// collab-integrate/src/collab_builder.rs
if build_config.sync_enable {
// SyncPlugin uses client_api::collab_sync for WebSocket transport
let sync_plugin = SyncPlugin::new(...);
collab_builder.with_plugin(Box::new(sync_plugin));
}
Providing Cloud Plugins via the Provider Trait
The provider abstraction enables runtime decisions about whether to load sync capabilities:
// cloud_service_impl.rs (excerpt) and plugin_provider.rs
impl CollabCloudPluginProvider for MyProvider {
fn is_sync_enabled(&self) -> bool { self.sync_enabled }
fn get_plugins(&self, ctx: CollabPluginProviderContext) -> Vec<Box<dyn CollabPlugin>> {
if ctx.is_cloud() && self.is_sync_enabled() {
vec![Box::new(SyncPlugin::new(...))]
} else {
vec![]
}
}
}
Server-Side WebSocket Handling
The server maintains per-client channels to broadcast CRDT operations:
// flowy-server/src/server.rs (line 142)
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let channel = Arc::new(WebSocketChannel::<ServerCollabMessage>::new(tx, rx));
self.clients.insert(uid, channel.clone());
Summary
- AppFlowy uses a CRDT-based sync stack (
y-crdt) to guarantee conflict-free real-time collaboration across clients. - The CollabBuilder in
collab_builder.rscontrols synchronization via thesync_enableconfiguration flag, defaulting to offline mode. - SyncPlugin attaches only when enabled, managing WebSocket connections through
client_api::collab_syncand abstracting network logic from the UI. - The CollabCloudPluginProvider trait enables runtime environment detection, supporting both cloud-synced and local-only execution modes.
- Server-side components in
flowy-server/src/server.rshandleServerCollabMessagestreams, broadcasting operations to all subscribed clients via WebSocket channels.
Frequently Asked Questions
What CRDT library does AppFlowy use for real-time collaboration?
AppFlowy builds its collaborative documents on y-crdt, a Rust implementation of the Yjs CRDT algorithm. The library handles operation transformation, conflict resolution, and state merging automatically, ensuring that concurrent edits from multiple users converge to the same document state without requiring a central authority to lock files.
How does AppFlowy handle offline editing?
When CollabBuilderConfig::sync_enable is set to false or the network is unavailable, the builder attaches NoSyncPlugin (or no plugin at all), allowing the Collab object to function purely locally using CollabKVDB for persistence. Once connectivity returns, managers can rebuild the object with sync_enable(true) to re-establish the WebSocket channel and sync pending changes.
Can individual documents disable real-time synchronization?
Yes. Real-time synchronization is opt-in per object. Managers for documents (flowy-document), folders (flowy-folder), and databases (flowy-database2) each control the sync_enable flag independently when calling build_collab(). This allows some workspace items to remain local-only while others collaborate live.
What message format travels over the WebSocket?
The WebSocket transports ServerCollabMessage structs defined in the client_api::collab_sync crate. These messages encapsulate compact binary operations (ops) generated by the CRDT layer, carrying update deltas, awareness state (cursors, selections), and sync acknowledgments between the client SyncPlugin and the cloud server.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →