How the Notification System and User Awareness Work in AppFlowy: A Deep Dive

AppFlowy uses a centralized notification framework (flowy-notification) to broadcast state changes from the Rust core to the Flutter UI, while the UserAwareness subsystem manages per-workspace ephemeral state as a Collab document that syncs between local disk and remote servers.

The notification system and user awareness in AppFlowy form the backbone of real-time collaboration and UI reactivity in the AppFlowy-IO/AppFlowy repository. This architecture decouples the core business logic written in Rust from the Flutter frontend, enabling cross-platform state synchronization while maintaining a lightweight observable pattern for user-specific transient data.

Notification System Architecture

NotificationBuilder and Sender Registration

At the heart of the system lies the NotificationBuilder located in frontend/rust-lib/flowy-notification/src/builder.rs. This struct constructs typed notification payloads containing an identifier, event type, source, and optional protobuf data or error information.

Concrete transport implementations are abstracted behind the NotificationSender trait defined in frontend/rust-lib/flowy-notification/src/lib.rs (lines 37-40). Whether the target is a Flutter MethodChannel or Tauri IPC, the core remains agnostic to the delivery mechanism.

Global Registry and Dispatch Flow

The runtime maintains a global registry (NOTIFICATION_SENDER) that holds all active Box<dyn NotificationSender> instances (lines 14-30 in lib.rs). Registration occurs via register_notification_sender, typically invoked during application initialization on the UI side.

When dispatching events, the system invokes send_subject (lines 82-94), which iterates over registered senders and forwards the constructed SubscribeObject:

// Conceptual flow inside send_subject
pub fn send_subject(subject: SubscribeObject) {
  let senders = NOTIFICATION_SENDER.read();
  for sender in senders.iter() {
    sender.send_subject(&subject);
  }
}

Typed Wrappers for User Events

To avoid boilerplate, the flowy-user crate provides convenient wrappers in frontend/rust-lib/flowy-user/src/notification.rs. The send_notification function constructs a NotificationBuilder pre-configured with the User observable source, while specialized variants like send_auth_state_notification handle authentication-specific events.

Usage sites throughout the codebase emit high-level events such as DidOpenWorkspace, DidUpdateUserProfile, and DidLoadUserAwareness. For example, when UserManager::open_workspace completes successfully, it notifies the UI layer:

// In frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs (lines 44-46)
let pb = UserProfilePB::from(profile);
send_notification(uid, UserNotification::DidOpenWorkspace)
    .payload(pb)
    .send();

User Awareness Subsystem

User awareness represents a per-workspace, per-user Collab document storing ephemeral collaborative state including reminders, cursor positions, and presence information. Unlike persistent document data, awareness data is transient and synchronized in real-time.

Lifecycle: Loading and Initialization

The awareness lifecycle begins in UserManager::open_workspace, which triggers initial_user_awareness (lines 34-46 in manager_user_workspace.rs). This asynchronous initialization sequence determines whether to hydrate the awareness state from local storage or fetch it from the cloud.

The initial_user_awareness method in frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs (lines 29-66) implements the following logic:

  1. Checks is_loading_awareness to prevent concurrent initialization
  2. Determines workspace type (local vs. remote)
  3. Routes to local disk or server accordingly

Local vs. Remote Awareness Loading

When workspace_type.is_local() returns true or a local file exists, the system builds the Collab document from disk using collab_for_user_awareness (lines 310-340). This method leverages the global AppFlowyCollabBuilder to instantiate a UserAwareness object with synchronization enabled.

For remote workspaces, load_awareness_from_server (lines 91-100) spawns an async task:

// From manager_user_awareness.rs
async fn load_awareness_from_server(&self, uid: i64, workspace_id: &Uuid, object_id: &str, workspace_type: WorkspaceType) {
  // Fetch document state from cloud service
  match self.cloud_service.get_user_awareness_doc_state(workspace_id).await {
    Ok(doc_state) => {
      let collab = self.collab_for_user_awareness(uid, workspace_id, object_id, doc_state).await?;
      self.user_awareness_by_workspace.insert(*workspace_id, collab);
      // Notify UI that awareness is ready
      send_notification(workspace_id, UserNotification::DidLoadUserAwareness);
    },
    Err(_) => {
      // Create fresh awareness if missing on server
      self.create_default_awareness(workspace_id).await?;
    }
  }
}

Integration with Collab Framework

The UserAwareness object itself resides in user_awareness_by_workspace, a concurrent map within UserManager (defined in mod.rs). Direct modifications—such as adding reminders—operate on this Collab instance:

// In manager_user_awareness.rs (lines 37-50)
pub async fn add_reminder(&self, reminder_pb: ReminderPB) -> FlowyResult<()> {
  let reminder = Reminder::from(reminder_pb);
  let workspace_id = self.workspace_id()?;
  let awareness = self.get_awareness(&workspace_id).await?;
  
  // Write to Collab document
  awareness.write().await.add_reminder(reminder.clone());
  
  // Persist through collab backend
  self.collab_interact.read().await.add_reminder(reminder).await?;
  Ok(())
}

While these mutations update the underlying Collab document immediately, the UI observes changes through the Collab framework's native observable subscriptions rather than explicit notifications. However, the initial load completion always triggers DidLoadUserAwareness as shown above.

Practical Code Examples

Registering a Notification Sender

On the Dart/Flutter side, you register a sender implementation that bridges to the Rust runtime via FFI:

void registerFlowySender() {
  final sender = FlowyNotificationSender(
    (SubscribeObject subject) {
      // Deserialize protobuf and dispatch to stream
      final notification = Notification.fromProto(subject);
      _notificationController.add(notification);
    },
  );
  // Register with Rust runtime
  rustApi.registerNotificationSender(sender);
}

This corresponds to the register_notification_sender function on the Rust side that populates the global NOTIFICATION_SENDER vector.

Emitting Workspace Notifications

When workspace settings change, the core emits typed notifications with optional payloads:

// Emitting a profile update notification
send_notification(uid, UserNotification::DidUpdateUserProfile)
    .payload(UserProfilePB::from(updated_profile))
    .send();

The send() call internally invokes send_subject, which distributes the message to all registered UI listeners.

Accessing UserAwareness Data

To read or modify awareness state after initialization:

// Retrieve awareness for current workspace
let awareness = user_manager.get_awareness(&workspace_id).await?;

// Read reminders
let reminders = awareness.read().await.get_all_reminders();

// Update presence (conceptual)
awareness.write().await.update_cursor_position(position);

Summary

  • Centralized dispatch: The flowy-notification crate provides a publisher-subscriber pattern where NotificationBuilder constructs payloads and send_subject dispatches to registered NotificationSender implementations.
  • Global registration: UI layers register transport implementations (Flutter channels, Tauri IPC) in the global NOTIFICATION_SENDER registry during app initialization.
  • UserAwareness lifecycle: Each workspace maintains a separate UserAwareness Collab document initialized via initial_user_awareness, supporting both local disk caching and remote server synchronization.
  • Typed events: The flowy-user crate exposes helper functions like send_notification to emit specific events (DidOpenWorkspace, DidLoadUserAwareness) that the Flutter UI observes to update widgets.
  • Collab integration: Awareness data persists through the Collab framework, enabling real-time synchronization of ephemeral state such as reminders and cursor positions across clients.

Frequently Asked Questions

How does AppFlowy notify the UI when a workspace opens?

When UserManager::open_workspace completes in manager_user_workspace.rs, it calls send_notification(uid, UserNotification::DidOpenWorkspace).payload(pb).send(). This constructs a NotificationBuilder that invokes send_subject, iterating over the global NOTIFICATION_SENDER registry to deliver the event to Flutter listeners via FFI.

What data is stored in the UserAwareness document?

The UserAwareness Collab object stores ephemeral collaborative state including user reminders, cursor positions, and presence information. Unlike permanent document content, this data is transient and synchronized in real-time across clients accessing the same workspace.

How does AppFlowy handle offline awareness loading?

The initial_user_awareness method in manager_user_awareness.rs checks if the workspace is local or if a cached file exists on disk. If available, it loads the Collab document locally via collab_for_user_awareness. Only if no local cache exists does it fall back to load_awareness_from_server, ensuring offline functionality.

Where is the notification sender implementation defined?

The NotificationSender trait is defined in frontend/rust-lib/flowy-notification/src/lib.rs (lines 37-40), while concrete implementations reside in the UI layer (Dart for Flutter). The Rust core stores these as Box<dyn NotificationSender> in the global NOTIFICATION_SENDER registry, registering them via register_notification_sender during application startup.

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 →