actualbudget/actual · error

Backup zip file is missing db.sqlite or metadata.json

Error message

Backup zip file is missing db.sqlite or metadata.json

What it means

A backup zip must contain both db.sqlite (the budget database) and metadata.json. loadBackup parses the zip successfully but, if either required entry is absent, it throws this error instead of restoring a partial or broken budget.

Source

Thrown at packages/loot-core/src/server/budgetfiles/backups.ts:246

      await cloudStorage.upload();
    } catch {}

    prefs.unloadPrefs();

    const zipContent = await fs.readFile(
      fs.join(budgetDir, 'backups', backupId),
      'binary',
    );

    let entries: Record<string, Uint8Array>;
    try {
      entries = safeUnzip(zipContent);
    } catch (e) {
      logger.log(e);
      throw new Error('Error reading backup zip file');
    }
    if (!entries['db.sqlite'] || !entries['metadata.json']) {
      throw new Error('Backup zip file is missing db.sqlite or metadata.json');
    }

    await fs.writeFile(fs.join(budgetDir, 'db.sqlite'), entries['db.sqlite']);
    await fs.writeFile(
      fs.join(budgetDir, 'metadata.json'),
      entries['metadata.json'],
    );
  }
}

export function startBackupService(id: string) {
  if (serviceInterval) {
    clearInterval(serviceInterval);
  }

  // Make a backup every 15 minutes
  serviceInterval = setInterval(
    async () => {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. List the zip contents (unzip -l backup.zip) and confirm both db.sqlite and metadata.json exist at the archive root.
  2. Re-zip the backup including both files, at the top level (no nested folder paths).
  3. Create a fresh backup from a working device (Settings > Backups / export) and restore that instead.
  4. If only db.sqlite exists, import the budget via the file-import flow rather than loadBackup.

Example fix

// before
// backup.zip contains only 'db.sqlite'
// after
// rebuild archive with both entries at root:
// unzip old.zip db.sqlite && zip backup.zip db.sqlite metadata.json
await send('load-backup', { id: backupId });
Defensive patterns

Strategy: validation

Validate before calling

const zip = await JSZip.loadAsync(zipContent);
const ok = zip.file('db.sqlite') && zip.file('metadata.json');
if (!ok) throw new Error('Backup must contain db.sqlite and metadata.json at the archive root');

Type guard

function hasRequiredEntries(entries: Record<string, unknown>): boolean {
  return Boolean(entries['db.sqlite']) && Boolean(entries['metadata.json']);
}

Try / catch

try {
  await send('load-backup', { id });
} catch (e) {
  if (e.message.includes('missing db.sqlite or metadata.json')) {
    console.error('Incompatible backup layout; re-export or re-zip with both files at root');
  } else throw e;
}

Prevention

When it happens

Trigger: Calling loadBackup with a zip produced by a different tool or older format that lacks metadata.json, or a user-zipped db.sqlite alone, or an archive that only contains metadata.json (e.g. an export that failed mid-way).

Common situations: Hand-rolled backups made by zipping just the db file; third-party scripts that back up only the database; restoring a backup from a much older Actual version with a different archive layout.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/15b8d282a77a4610. Report an issue: GitHub.