How AppFlowy Cloud Sync and Server Integration Works: Architecture and Implementation

AppFlowy Cloud sync and server integration uses a provider-based abstraction layer that lazily instantiates either a local mock server or a remote AppFlowy Cloud backend based on the authentication type, enabling seamless two-way synchronization between local SQLite storage and the cloud.

AppFlowy stores all user data locally in an SQLite database while maintaining optional synchronization with AppFlowy Cloud through a sophisticated Rust-based server layer. This article examines the exact mechanisms that handle authentication, server selection, and workspace synchronization as implemented in the AppFlowy-IO/AppFlowy repository.

Core Architecture: The Server Provider Pattern

The foundation of AppFlowy Cloud sync relies on a type-safe server provider that abstracts away network implementation details from the UI layer.

Server Selection Based on AuthType

The system distinguishes between offline and cloud modes using the AuthType enum defined in flowy_user_pub::entities.rs. The ServerProvider struct in frontend/rust-lib/flowy-core/src/server_layer.rs (lines 27-73) maintains a map of authentication types to server implementations, lazily creating the appropriate backend when first requested:

pub fn get_server(&self) -> FlowyResult<Arc<dyn AppFlowyServer>> {
    let auth_type = self.get_auth_type();
    if let Some(r) = self.providers.get(&auth_type) {
        return Ok(r.value().clone());
    }
    
    let server: Arc<dyn AppFlowyServer> = match auth_type {
        AuthType::Local => Arc::new(LocalServer::new(...)),
        AuthType::AppFlowyCloud => {
            let cfg = self.config.cloud_config.clone()
                .ok_or_else(|| FlowyError::internal().with_context("Missing cloud config"))?;
            Arc::new(AppFlowyCloudServer::new(cfg, ...))
        },
    };
    self.providers.insert(auth_type, server);
    Ok(self.providers.get(&auth_type).unwrap().clone())
}

When AuthType::AppFlowyCloud is active, the provider returns an AppFlowyCloudServer instance (defined in frontend/rust-lib/flowy-server/src/af_cloud/server.rs, lines 33-50) that communicates via HTTP API. For offline usage or tests, it instantiates LocalServer from frontend/rust-lib/flowy-server/src/local_server.rs.

Unified Cloud Service Interface

All cloud operations flow through the AppFlowyServer trait (lines 62-71 of frontend/rust-lib/flowy-server/src/server.rs), which exposes specialized services for users, folders, documents, databases, AI, and storage. The user-specific subset is defined in the UserCloudService trait located in frontend/rust-lib/flowy-user-pub/src/cloud.rs (lines 124-150), providing methods for sign-in, workspace creation, and member management.

The UserCloudServiceProvider trait (same file, lines 62-74) abstracts token handling and server retrieval. ServerProvider implements this in frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs (lines 65-78), delegating to the underlying AppFlowyServer:

impl UserCloudServiceProvider for ServerProvider {
    fn set_server_auth_type(&self, auth_type: &AuthType, token: Option<String>) -> FlowyResult<()> {
        self.set_auth_type(*auth_type);
        if let Some(tok) = token { self.set_token(&tok)?; }
        Ok(())
    }
    
    fn get_user_service(&self) -> Result<Arc<dyn UserCloudService>, FlowyError> {
        Ok(self.get_server()?.user_service())
    }
}

Workspace Synchronization Flow

When a user opens a workspace, the system orchestrates a coordinated sync between local SQLite storage and the remote cloud state.

Opening a Workspace with Cloud Sync

The UserManager::open_workspace method in frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs (lines 141-199) handles the complete initialization sequence:

  1. Authentication setup – Configures the server provider with the current AuthType and retrieves the access token.
  2. Local database check – Queries SQLite for existing workspace data using select_user_workspace.
  3. Cloud reconciliation – If the workspace is missing locally, calls sync_workspace to fetch from the cloud.
  4. Background refresh – If local data exists, spawns a tokio::spawn task to re-sync with the server asynchronously.
pub async fn open_workspace(&self, workspace_id: &Uuid, workspace_type: WorkspaceType) -> FlowyResult<()> {
    let auth_type = AuthType::from(workspace_type);
    let token = self.token_from_auth_type(&auth_type)?;
    let cloud_service = self.cloud_service()?;
    cloud_service.set_server_auth_type(&auth_type, token)?;

    let profile = self.get_user_profile_from_disk(uid, &workspace_id_str).await?;
    cloud_service.set_token(&profile.token)?;

    let user_workspace = match select_user_workspace(&workspace_id_str, &mut conn) {
        Err(err) if err.is_record_not_found() => {
            sync_workspace(workspace_id, cloud_service.get_user_service()?, uid, workspace_type, self.db_pool(uid)?).await?
        },
        Ok(row) => {
            let user_workspace = UserWorkspace::from(row);
            let pool = self.db_pool(uid)?;
            tokio::spawn(async move {
                let _ = sync_workspace(workspace_id, user_service, uid, workspace_type, pool).await;
            });
            user_workspace
        }
    };
    // UI notification and awareness initialization follow...
}

The Sync Implementation

The sync_workspace function (lines 809-821 of the same file) pulls the latest workspace state from the cloud and persists it locally:

async fn sync_workspace(
    workspace_id: &Uuid,
    user_service: Arc<dyn UserCloudService>,
    uid: i64,
    workspace_type: WorkspaceType,
    pool: Arc<ConnectionPool>,
) -> FlowyResult<UserWorkspace> {
    let user_workspace = user_service.open_workspace(workspace_id).await?;
    if let Ok(mut conn) = pool.get() {
        upsert_user_workspace(uid, workspace_type, user_workspace.clone(), &mut conn)?;
    }
    Ok(user_workspace)
}

For settings updates, sync_workspace_settings (lines 453-473) pushes local changes to the cloud via user_service.set_workspace_settings before updating the local SQLite cache.

Implementation Example: Initializing Cloud Sync

The following example demonstrates how to initialize the server provider and open a cloud-enabled workspace:

use flowy_core::AppFlowyCoreConfig;
use flowy_core::server_layer::ServerProvider;
use flowy_user::user_manager::UserManager;
use uuid::Uuid;
use flowy_user_pub::entities::WorkspaceType;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let config = AppFlowyCoreConfig::load().await?;
    
    let server_provider = ServerProvider::new(
        config,
        Default::default(),
        flowy_server::guest_dto::LoggedUserDummy,
        None,
    );

    let user_manager = UserManager::new(server_provider.clone());
    
    let workspace_id = Uuid::parse_str("9f1e2c6a-b3f8-4d5a-a9d2-c8e2d8f9b4a1")?;
    user_manager
        .open_workspace(&workspace_id, WorkspaceType::Personal)
        .await?;

    println!("Workspace opened & synced!");
    Ok(())
}

This initialization creates the lazy server map, but the actual AppFlowyCloudServer instantiation occurs only when open_workspace triggers get_server() with AuthType::AppFlowyCloud.

Summary

  • Lazy server initialization: The ServerProvider delays cloud connection establishment until explicitly requested, supporting both LocalServer (offline) and AppFlowyCloudServer (remote) modes through the unified AppFlowyServer trait.
  • Type-safe authentication: The AuthType enum drives server selection, ensuring that token handling and service instantiation remain consistent across the application.
  • Two-tier synchronization: Workspace opening performs an immediate blocking sync if data is missing locally, followed by a non-blocking background refresh via tokio::spawn to maintain eventual consistency without freezing the UI.
  • Unified service traits: All cloud operations flow through UserCloudServiceProvider and UserCloudService abstractions, decoupling the UI from HTTP implementation details defined in flowy-server/src/af_cloud/.

Frequently Asked Questions

How does AppFlowy handle offline mode versus cloud sync?

AppFlowy uses the AuthType enum to distinguish between Local and AppFlowyCloud modes. When AuthType::Local is set, the ServerProvider returns a LocalServer stub that satisfies the AppFlowyServer trait without making network calls, allowing full functionality with only SQLite storage. Switching to AuthType::AppFlowyCloud lazily instantiates the real cloud client and initiates synchronization.

What happens when a workspace is opened for the first time on a new device?

The UserManager::open_workspace method checks the local SQLite database via select_user_workspace. If no record exists (indicated by is_record_not_found()), it blocks the UI thread to call sync_workspace, which fetches the complete workspace state from user_service.open_workspace and persists it using upsert_user_workspace. Subsequent opens trigger a background refresh instead.

Where is the cloud synchronization logic located in the codebase?

The primary synchronization logic resides in three key locations: frontend/rust-lib/flowy-core/src/server_layer.rs handles server selection and caching; frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs contains the open_workspace orchestration and sync_workspace implementation; and frontend/rust-lib/flowy-server/src/af_cloud/server.rs provides the concrete HTTP client for AppFlowy Cloud operations.

How does AppFlowy ensure data consistency between local and remote storage?

The system implements eventual consistency through a combination of immediate blocking fetches for missing data and fire-and-forget background updates. When local changes occur, dedicated sync methods like sync_workspace_settings push updates immediately. For incoming changes, a background tokio::spawn task refreshes the local SQLite cache without blocking user interaction, ensuring the UI remains responsive while maintaining synchronization.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →