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:
- Checks
is_loading_awarenessto prevent concurrent initialization - Determines workspace type (local vs. remote)
- 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-notificationcrate provides a publisher-subscriber pattern whereNotificationBuilderconstructs payloads andsend_subjectdispatches to registeredNotificationSenderimplementations. - Global registration: UI layers register transport implementations (Flutter channels, Tauri IPC) in the global
NOTIFICATION_SENDERregistry during app initialization. - UserAwareness lifecycle: Each workspace maintains a separate
UserAwarenessCollab document initialized viainitial_user_awareness, supporting both local disk caching and remote server synchronization. - Typed events: The
flowy-usercrate exposes helper functions likesend_notificationto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →