Skip to main content

State Migrations

State migrations allow you to transform stored state data when the structure changes between versions. This is critical for maintaining compatibility as the application evolves.

Migration System

Bitwarden uses a versioned migration system located in libs/common/src/state-migrations/.

Migration Structure

Each migration:
  • Has a version number
  • Transforms state from version N to N+1
  • Runs automatically on application startup
import { Migration } from "../migration";

export class MyMigration extends Migration {
  version = 42; // Migration version

  async migrate(helper: MigrationHelper): Promise<void> {
    // Read old state
    const oldValue = await helper.get<OldType>("oldKey");
    
    // Transform data
    const newValue = this.transform(oldValue);
    
    // Write new state
    await helper.set("newKey", newValue);
    
    // Remove old state
    await helper.remove("oldKey");
  }

  private transform(old: OldType): NewType {
    // Transformation logic
    return {
      newField: old.oldField,
      // ... other transformations
    };
  }
}

MigrationHelper

The MigrationHelper provides methods for state manipulation during migrations:
interface MigrationHelper {
  get<T>(key: string): Promise<T | null>;
  set<T>(key: string, value: T): Promise<void>;
  remove(key: string): Promise<void>;
  getAccounts(): Promise<Account[]>;
}

Migration Best Practices

Always test migrations with real data from previous versions. Data loss during migration can be catastrophic.

1. Preserve Data

Never delete data without preserving it elsewhere first:
// Good - preserve data
const oldValue = await helper.get("oldKey");
await helper.set("newKey", transform(oldValue));
await helper.remove("oldKey");

// Bad - risk of data loss
await helper.remove("oldKey");
await helper.set("newKey", defaultValue);

2. Handle Missing Data

Always handle cases where expected data doesn’t exist:
const value = await helper.get("key");
if (value == null) {
  // Provide sensible defaults
  await helper.set("key", defaultValue);
  return;
}

3. Test Incrementally

Migrations run sequentially, so test each migration independently:
// Test migration 42
const result = await runMigration(42, testData);
expect(result).toMatchSnapshot();

Example Migration

Here’s a real example migrating vault timeout settings:
export class VaultTimeoutMigration extends Migration {
  version = 15;

  async migrate(helper: MigrationHelper): Promise<void> {
    const accounts = await helper.getAccounts();

    for (const account of accounts) {
      // Get old timeout value (in minutes)
      const oldTimeout = await helper.get<number>(
        `${account.id}_vaultTimeout`
      );

      if (oldTimeout != null) {
        // Convert to new structure (in milliseconds)
        const newTimeout = {
          value: oldTimeout * 60 * 1000,
          action: "lock",
        };

        await helper.set(`${account.id}_vaultTimeoutSettings`, newTimeout);
        await helper.remove(`${account.id}_vaultTimeout`);
      }
    }
  }
}

Build docs developers (and LLMs) love