Skip to main content

State Management

The @bitwarden/state library provides a comprehensive State Provider Framework for centralized application state management across Bitwarden clients.

Overview

The State Provider Framework was designed to:
  • Enable domain ownership - Teams own their state definitions
  • Enforce best practices - Reduce boilerplate and prevent common mistakes
  • Support account switching - Built-in multi-account support
  • Provide trustworthy observables - Reliable reactive state streams
  • Simplify testing - Comprehensive fake/mock implementations

Core Concepts

State Storage Locations

State can be stored in two primary locations:
  • Disk ("disk") - Persistent storage (survives app restarts)
  • Memory ("memory") - In-memory cache (cleared on app restart)
Client-Specific Locations:
  • Web: "disk" defaults to session storage, "disk-local" for local storage
  • Desktop/Browser: Platform-specific persistent storage

State Scopes

  • Global State - Application-wide state (not user-specific)
  • User State - State scoped to individual users
  • Active User State - State for currently active user (deprecated)
  • Derived State - Computed state based on other state

State Definitions

StateDefinition

StateDefinition defines a storage location and top-level namespace. Location: Teams add entries to a central state-definitions.ts file
Important Rules:
  • Use camelCase for state names
  • Names must be unique per storage location
  • Same name can be used for both disk and memory
  • Never change StateDefinition names for disk storage without migration

KeyDefinition and UserKeyDefinition

KeyDefinition and UserKeyDefinition specify individual state elements.

UserKeyDefinition (User-Scoped State)

KeyDefinition (Global State)

Complex State with Deserializers

Array and Record Helpers

Key Definition Options

State Provider

StateProvider is the main service for accessing state.

Getting State

Alternative: Specific Providers

For lighter dependencies, inject specific providers:

Working with State

GlobalState<T>

SingleUserState<T>

Reading State

Updating State

Update Options

shouldUpdate: Prevent Unnecessary Updates

combineLatestWith: Conditional Updates

Derived State

Derived state caches expensive computations based on other state.

DeriveDefinition

Using Derived State

Force Derived Value

Useful for clearing derived state during logout:

State Migrations

Migrate data when changing state definitions or structure.

Creating a Migration

Location: libs/state/src/state-migrations/migrations/

Migration Best Practices

  1. Never skip migrations - Run all migrations in order
  2. Test thoroughly - Migrations are irreversible
  3. Use KeyDefinitionLike - Avoid importing from application code
  4. Handle null data - Users may not have data to migrate

Why Not ActiveUserState?

ActiveUserState is deprecated due to race condition issues.

Problem: Account Switching Race Condition

Solution: Use SingleUserState

Benefits of SingleUserState

  1. No race conditions - Operations always target same user
  2. Flexible API - Can query any user’s data
  3. Better account switching - Clean transitions with switchMap

Testing

Fake State Provider

Best Practices

State Definition

  1. Choose appropriate storage:
    • Disk for persistent data
    • Memory for caches/computed data
  2. Set proper clearOn events:
    • ["logout"] for sensitive data
    • ["lock", "logout"] for very sensitive data
    • [] for settings that survive logout
  3. Write good deserializers:
    • Handle null and undefined
    • Convert dates/complex types correctly
    • Test edge cases

State Updates

  1. Use shouldUpdate when possible:
    • Reduces unnecessary I/O
    • Prevents redundant observable emissions
  2. Avoid firstValueFrom() after updates:
    • Use return value from update()
    • Or stay in reactive observable world
  3. Handle null in update functions:
    • State can be null or undefined
    • Provide defaults when needed

Observable Patterns

  1. Don’t leave reactivity:
    • Propagate observables through your app
    • Use async pipe in templates
    • Avoid premature subscription
  2. Clean account switching:
    • Use switchMap with activeAccount$
    • Combine streams with same user ID
  • @bitwarden/state-internal - Internal state implementation (do not use directly)
  • @bitwarden/state-test-utils - Testing utilities
  • @bitwarden/storage-core - Storage service implementations
  • Platform Library - Storage abstractions

State Architecture Diagram

See libs/state/state_diagram.svg for visual architecture overview.

Source Code

  • State Library: libs/state/
  • State Internal: libs/state-internal/
  • State Migrations: libs/state/src/state-migrations/migrations/
  • Storage Core: libs/storage-core/
  • Test Utils: libs/state-test-utils/

Additional Resources