lobehub/lobehub · critical · Error

Local database migration ${applied.version} (${applied.name}

Error message

Local database migration ${applied.version} (${applied.name}) differs from the application manifest

What it means

Migration runner detects that an already-applied migration's recorded name or sha256 checksum does not match the application's current manifest entry for the same version. This guards against silent edits to a released migration (which would corrupt the audit trail) and against a manifest whose statements were rewritten after deployment.

Source

Thrown at apps/desktop/src/main/database/migrations/runner.ts:74

  for (const [index, applied] of appliedMigrations.entries()) {
    const expectedVersion = index + 1;

    if (applied.version !== expectedVersion) {
      throw new Error(
        `Local database migration history is not contiguous: expected version ${expectedVersion}, received ${applied.version}`,
      );
    }

    const migration = migrations[applied.version - 1];

    if (!migration) {
      throw new Error(
        `Local database version ${applied.version} is newer than this application supports`,
      );
    }

    if (applied.name !== migration.name || applied.checksum !== migrationChecksum(migration)) {
      throw new Error(
        `Local database migration ${applied.version} (${applied.name}) differs from the application manifest`,
      );
    }
  }
};

const applyMigration = (database: DatabaseSync, migration: LocalDatabaseMigration) => {
  database.exec('BEGIN IMMEDIATE');

  try {
    for (const statement of migration.statements) database.exec(statement);

    database
      .prepare(
        `INSERT INTO ${MIGRATIONS_TABLE} (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)`,
      )
      .run(migration.version, migration.name, migrationChecksum(migration), Date.now());
    database.exec('COMMIT');

View on GitHub (pinned to 10f24d7ade)

Solutions

  1. Revert the edit to the migration file so its name + statements match the version the DB recorded — this is the correct fix in 99% of cases.
  2. If the edit is intentional and required, ship it as a NEW migration (next version) instead of editing the old one, so checksums stay stable.
  3. If the DB itself is wrong (e.g. you know the applied migration is corrupt), back it up and repair the __local_database_migrations row with the correct name + checksum — only as a last resort.
  4. Add a pre-commit hook that fails if migration files older than the latest version are modified.
  5. Run the runner with logging that prints applied vs manifest checksum side-by-side to confirm the diagnosis.

Example fix

// before
if (applied.name !== migration.name || applied.checksum !== migrationChecksum(migration)) {
  throw new Error(`Local database migration ${applied.version} (${applied.name}) differs from the application manifest`);
}

// after — show which field diverged and both checksums for diagnosis
const manifestChecksum = migrationChecksum(migration);
if (applied.name !== migration.name || applied.checksum !== manifestChecksum) {
  throw new Error(
    `Local database migration ${applied.version} differs from manifest: ` +
    `name applied='${applied.name}' manifest='${migration.name}'; ` +
    `checksum applied=${applied.checksum} manifest=${manifestChecksum}`,
  );
}
Defensive patterns

Strategy: validation

Validate before calling

import { createHash } from 'node:crypto';

function manifestChecksumMatches(applied: { name: string; checksum: string }, migration: { version: number; name: string; statements: unknown[] }): boolean {
  const expected = createHash('sha256').update(JSON.stringify([migration.version, migration.name, migration.statements])).digest('hex');
  return applied.name === migration.name && applied.checksum === expected;
}

Type guard

function isAppliedMigration(value: unknown): value is { version: number; name: string; checksum: string } {
  return typeof value === 'object' && value !== null
    && typeof (value as any).version === 'number'
    && typeof (value as any).name === 'string'
    && typeof (value as any).checksum === 'string';
}

Try / catch

try {
  runLocalDatabaseMigrations(database);
} catch (e) {
  if (e instanceof Error && /differs from the application manifest/.test(e.message)) {
    // a migration file was edited after release — revert the file or add a new migration instead
    showMigrationChecksumMismatchDialog();
    app.quit();
  } else throw e;
}

Prevention

When it happens

Trigger: A developer edits statements of an already-shipped migration after users have applied it; the migration's `name` field was renamed; the migration order was reshuffled but the version numbers kept contiguous; the DB was migrated by a different fork whose migration shared a version number but different SQL.

Common situations: Rebasing a feature branch that touched migration files; a cherry-pick that introduced conflicting migration edits; a manual SQL hotfix that did not update the checksum; a fork/divergent build writing to the same DB.

Related errors


AI-assisted analysis of lobehub/lobehub@10f24d7ade (2026-08-12). Data as JSON: /api/errors/656b98c79d9f0d04. Report an issue: GitHub.