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
- List the zip contents (unzip -l backup.zip) and confirm both db.sqlite and metadata.json exist at the archive root.
- Re-zip the backup including both files, at the top level (no nested folder paths).
- Create a fresh backup from a working device (Settings > Backups / export) and restore that instead.
- 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
- Always create backups through the app's export rather than hand-zipping.
- Confirm both files sit at the zip root, not inside a subfolder.
- Include metadata.json in any scripted backup of db.sqlite.
- Test a restore from each backup archive you keep.
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
- Error reading backup zip file
- Error exporting budget: ${result.error}
- zipMeta ? getUnsafeZipError(zipMeta) : error
- Invalid catalog format: expected an array
- Table "${tableName}" does not exist in the schema
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/15b8d282a77a4610.
Report an issue: GitHub.