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 store
  • set_session(None) – Clears the cached entry and removes the session from local storage
  • user_id() – Returns the user_id from the active Session
  • is_local_mode() – Inspects the workspace type (WorkspaceType::Local) to determine if running offline
  • get_collab_db(uid) – Returns a weak reference to the on-disk collaborative storage for the specified user
  • close_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:

  1. UI triggers AuthService.signInWithEmailPassword(email, password)
  2. BackendAuthService builds a SignInPayloadPB (containing email, password, authType, and device ID) and dispatches UserEventSignInWithEmailPassword
  3. The Rust core receives the event, validates credentials, creates a new Session, stores it in the KV store via AuthenticateUser, and returns a GotrueTokenResponsePB
  4. On success, the UI calls UserBackendService.getCurrentUserProfile() to fetch the UserProfilePB (containing uid, name, email, icon_url)
  5. Subsequent launches read the cached session via AuthenticateUser::get_session without 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-user crate) 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 AuthenticateUser struct in authenticate_user.rs controls session lifecycle, workspace context, and database connections
  • Frontend abstraction: AuthService and UserBackendService provide type-safe Dart APIs that dispatch to Rust events like UserEventSignInWithEmailPassword and UserEventUpdateUserProfile
  • Device tracking: Authentication payloads include unique device IDs generated via device_id.dart for 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:

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 →