# How AppFlowy Handles User Authentication and User Profile Management: Rust Core and Flutter Architecture

> Discover how AppFlowy handles user authentication and profile management with its Rust core and Flutter frontend. Learn about its offline-first local mode and cloud-backed capabilities.

- Repository: [AppFlowy-IO/AppFlowy](https://github.com/AppFlowy-IO/AppFlowy)
- Tags: internals
- Published: 2026-03-03

---

**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`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/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

```dart
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

```dart
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

```dart
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

```dart
// 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`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/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`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/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.