How AppFlowy Handles User Authentication and User Profile Management: Rust Core and Flutter Architecture
AppFlowy implements user authentication and user profile management through a split architecture where a Rust-based core manages local session persistence while a Flutter frontend communicates via protobuf events to support both offline-first local mode and cloud-backed authentication.
The AppFlowy-IO/AppFlowy repository uses a sophisticated dual-layer approach to handle user authentication and user profile management, combining the performance and safety of Rust with the UI flexibility of Flutter. This architecture ensures users can work completely offline while maintaining the capability to sync with AppFlowy Cloud when connectivity is available.
Core Session Handling in the Rust Backend
The foundation of AppFlowy’s authentication system resides in the flowy-user crate, where the AuthenticateUser struct manages the entire session lifecycle. Located in frontend/rust-lib/flowy-user/src/services/authenticate_user.rs, this module provides low-level APIs for creating, persisting, and clearing user sessions.
The Session Struct and Local Persistence
At the heart of the system is the Session object, which stores the signed-in user ID, workspace ID, and device information. When a user authenticates, the core creates or restores this session from a KV store (KVStorePreferences) using the session cache key defined in UserConfig. The session is cached in an ArcSwapOption for thread-safe access across the application.
The core automatically handles legacy session migration via migrate_session and establishes the user’s SQLite database connection upon session creation. All session information persists locally, enabling the offline-first experience that characterizes AppFlowy’s architecture.
Session Lifecycle Operations
The AuthenticateUser struct exposes precise methods for managing the authentication state:
set_session(session)– Caches the session in memory and persists it to the KV storeset_session(None)– Clears the cached entry and removes the session from local storageuser_id()– Returns theuser_idfrom the activeSessionis_local_mode()– Inspects the workspace type (WorkspaceType::Local) to determine if running offlineget_collab_db(uid)– Returns a weak reference to the on-disk collaborative storage for the specified userclose_db()– Flushes and closes the user’s SQLite connection cleanly
Workspace-specific helpers like workspace_id(), workspace_type(), and workspace_database_object_id() read the current workspace context from the SQLite DB via SQL helpers (select_user_workspace, select_user_workspace_type).
Flutter Frontend Authentication Architecture
The Flutter layer abstracts all authentication operations through the AuthService interface defined in frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart. This abstraction allows the UI to remain agnostic of whether the app is running in local or cloud mode.
BackendAuthService and Protobuf Events
The concrete implementation, BackendAuthService (backend_auth_service.dart), translates Dart method calls into protobuf events dispatched to the Rust core. Each authentication action maps to a specific event:
- Sign-in with email/password →
UserEventSignInWithEmailPassword - Sign-up (including guest) →
UserEventSignUp - Sign-out →
UserEventSignOut - Magic-link sign-in →
UserEventMagicLinkSignIn - Passcode sign-in →
UserEventPasscodeSignIn
The service injects a device ID via device_id.dart so the backend can associate sessions with specific hardware. All methods return a FlowyResult<T, FlowyError> that enables the UI to handle success and error states through pattern matching.
User Profile Operations via UserBackendService
Profile management flows through UserBackendService (user_service.dart), which provides static helpers for account operations without requiring service instantiation:
| Action | Backend Event | Method |
|---|---|---|
| Get current profile | UserEventGetUserProfile |
getCurrentUserProfile() |
| Update profile fields | UserEventUpdateUserProfile |
updateUserProfile({name, email, iconUrl}) |
| Anonymous user creation | UserEventOpenAnonUser |
openAnonUser() |
| Delete account | UserEventDeleteAccount |
deleteCurrentAccount() |
End-to-End Authentication Flow
The complete sign-in process demonstrates how the layers interact:
- UI triggers
AuthService.signInWithEmailPassword(email, password) BackendAuthServicebuilds aSignInPayloadPB(containing email, password,authType, and device ID) and dispatchesUserEventSignInWithEmailPassword- The Rust core receives the event, validates credentials, creates a new
Session, stores it in the KV store viaAuthenticateUser, and returns aGotrueTokenResponsePB - On success, the UI calls
UserBackendService.getCurrentUserProfile()to fetch theUserProfilePB(containinguid,name,email,icon_url) - Subsequent launches read the cached session via
AuthenticateUser::get_sessionwithout network requests
Managing User Profiles
Profile data lives in the SQLite DB managed by UserDB and mirrors to the cloud when using AppFlowy Cloud workspaces.
Reading profiles requires calling UserBackendService.getCurrentUserProfile(), which sends UserEventGetUserProfile and returns a UserProfilePB object containing user metadata and avatar URLs.
Updating profiles uses UserBackendService.updateUserProfile(), which constructs an UpdateUserProfilePayloadPB with the current userId and dispatches UserEventUpdateUserProfile. This supports partial updates—modifying only the name, email, or icon_url fields as needed.
Workspace context remains accessible through the core’s AuthenticateUser.workspace_id() method, which reads from the active session; the Flutter side can alternatively retrieve active workspace details via UserBackendService.getCurrentWorkspace().
Implementation Examples
Sign-in with Email and Password
final authService = BackendAuthService(AuthTypePB.Local);
final result = await authService.signInWithEmailPassword(
email: 'john@example.com',
password: 's3cr3t',
);
result.fold(
(token) async {
// Authentication succeeded – fetch the profile
final profileResult = await UserBackendService.getCurrentUserProfile();
profileResult.map((profile) => print('Welcome ${profile.name}'));
},
(error) => print('Sign‑in failed: ${error.msg}'),
);
Retrieve Current User Profile
final profileResult = await UserBackendService.getCurrentUserProfile();
profileResult.fold(
(profile) {
print('User: ${profile.name}');
print('Email: ${profile.email}');
print('Avatar URL: ${profile.iconUrl}');
},
(error) => print('Failed to load profile: ${error.msg}'),
);
Update Profile Information
final updateResult = await UserBackendService(userId: currentUid)
.updateUserProfile(name: 'Jane Doe', iconUrl: 'https://example.com/avatar.png');
updateResult.fold(
(_) => print('Profile updated!'),
(error) => print('Update failed: ${error.msg}'),
);
Anonymous User Flow
// Create an anonymous user
await UserBackendService.openAnonUser();
// Retrieve the anon profile
final anonProfile = await UserBackendService.getAnonUser();
Summary
- Split architecture: Rust core (
flowy-usercrate) handles session persistence and SQLite operations, while Flutter manages UI interactions via protobuf events - Offline-first design: Sessions store locally in
KVStorePreferences, allowing full functionality without network connectivity - Session management: The
AuthenticateUserstruct inauthenticate_user.rscontrols session lifecycle, workspace context, and database connections - Frontend abstraction:
AuthServiceandUserBackendServiceprovide type-safe Dart APIs that dispatch to Rust events likeUserEventSignInWithEmailPasswordandUserEventUpdateUserProfile - Device tracking: Authentication payloads include unique device IDs generated via
device_id.dartfor multi-device session management
Frequently Asked Questions
How does AppFlowy handle offline authentication?
AppFlowy uses an offline-first architecture where the Rust core persists session data to a local KV store (KVStorePreferences) and SQLite database. When authenticating locally, the AuthenticateUser struct creates a Session with WorkspaceType::Local, allowing users to work without internet connectivity. The session cache survives app restarts via get_session, eliminating the need for repeated authentication until the user explicitly signs out or the session expires.
What storage mechanisms secure user session data?
Session data is stored in two layers: the KV store (KVStorePreferences) for lightweight session metadata (user ID, workspace ID, device info) and SQLite for user profiles and collaborative data. The AuthenticateUser struct manages both, using ArcSwapOption for thread-safe in-memory caching and providing close_db() for graceful shutdowns that prevent data corruption.
How does the Flutter layer communicate authentication requests to Rust?
Communication occurs through protobuf-based events. The Flutter side constructs payloads (like SignInPayloadPB or UpdateUserProfilePayloadPB) and dispatches them via the event system (e.g., UserEventSignInWithEmailPassword). The BackendAuthService handles the marshaling, while the Rust core processes the events in authenticate_user.rs and returns strongly-typed responses wrapped in FlowyResult for error handling.
Can AppFlowy manage multiple workspaces per user session?
Yes. The core stores a workspace_id within the Session object, and the AuthenticateUser struct provides helpers like workspace_id(), workspace_type(), and workspace_database_object_id() to retrieve current workspace context from the SQLite database. Users can switch workspaces, and the system maintains separate collaborative databases (get_collab_db) per user-workspace combination, though the active workspace is bound to the current session context.
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 →