Fix it now
- Drop the file
- Repair runs locally
- Download the result
Signal Desktop stores your entire message history in a single SQLite database called db.sqlite, but it is not plain SQLite — Signal uses SQLCipher, a fork that transparently AES-256-encrypts the whole file, header and all. That means a page-salvage engine (this one included) sees high-entropy noise and reports file is not a database until the file is decrypted. So recovering a broken Signal database is two steps: first decrypt it locally with the key that is already on your machine, producing a plaintext SQLite file; then drop that plaintext file above and the tool walks its pages in your browser and rebuilds a clean database from every row it can still decode. The key and the data both stay on your device.
Signal's db.sqlite is encrypted — decrypt it before any salvage
Signal Desktop's database lives at ~/Library/Application Support/Signal/sql/db.sqlite on macOS, %AppData%\Signal\sql\db.sqlite on Windows, and ~/.config/Signal/sql/db.sqlite on Linux. Because SQLCipher encrypts the entire file, running file db.sqlite shows random bytes rather than the usual SQLite format 3\0 magic. Any plain SQLite tool — the sqlite3 shell, DB Browser, or this page's salvage engine — sees ciphertext, not pages, and cannot and will not guess the key.
The 64-hex-character SQLCipher key sits in config.json next to the database. On older builds it is stored in the clear under a key field; since 2024 Signal wraps it under encryptedKey using Electron's safeStorage API — backed by the macOS Keychain item named "Signal Safe Storage", DPAPI on Windows, and gnome-libsecret / kwallet on Linux. Because the wrapper is tied to your OS account, you unwrap the key on the same machine and user that created it.
With the raw key in hand, open the database in any SQLCipher-aware tool (the sqlcipher CLI, or a DB Browser for SQLite build compiled with SQLCipher) and run PRAGMA key = "x'<64-hex-key>'"; followed by PRAGMA cipher_compatibility = 4; — Signal ships SQLCipher 4. Then export a plaintext copy with .recover or VACUUM INTO 'plain.sqlite'. That plaintext file is what you drop here. Work from a byte-for-byte copy, never your only original.
What "malformed" means inside a Signal database
Once decrypted, a Signal database is an ordinary SQLite file: a flat array of fixed-size pages (4096 bytes each). Page 1 holds the header and schema; every table and index is a b-tree of interior pages pointing down to leaf pages that hold the rows. Signal's data lives in tables like messages (each row keyed by id, with conversationId, sent_at, received_at, type, a body column and a json column carrying the full message record), conversations, reactions, sessions, items, and a full-text messages_fts FTS5 index used for search.
Corruption is almost always localized. A power loss mid-write, an interrupted sync, a torn page on storage that lied about flushing — one pointer in one b-tree stops agreeing with the data, and SQLite raises SQLITE_CORRUPT: Error: database disk image is malformed (11). It refuses the whole file the instant it can't trust the structure, even though your other tables are sitting untouched in their leaf pages. The file looks far worse than it is.
The related error file is not a database (SQLITE_NOTADB, code 26) has two meanings here: either the 16-byte header magic was damaged (the pages behind it are often intact), or — far more common with Signal — you pointed a plain tool at the still-encrypted file. If you see it before decrypting, that is the encryption talking, not damage.
What page-salvage gets back from a Signal DB — and what it can't
Page-salvage does not fix the broken file in place; it does what SQLite's own .recover does. It walks the decrypted file page by page, and for every page that looks like a table leaf it reads each cell directly — rowid, serial types, values — and re-inserts every decodable row into a brand-new, empty database. Damaged pages are skipped; readable pages give up their rows whether or not the b-tree that indexed them survived. Rows the pointer chain can no longer reach are found by a raw sweep, and rows whose parent table can't be identified go to a lost_and_found table keyed by page and cell rather than being thrown away. In practice your messages rows come back with their body and json intact even when the FTS index or one conversation's tree was the thing that broke.
If the crash left a sibling -wal file (WAL mode stages the newest committed transactions before they are checkpointed into db.sqlite), it holds your most recent messages — but it is SQLCipher-encrypted too, so decrypt it together with the main file. Given a plaintext database and its plaintext WAL, the engine overlays the committed frames before the scan so recovery reflects the last committed transaction.
The honest limits are real. Attachments are not in the database: Signal stores photos, video and files as separate, individually-encrypted blobs under attachments.noindex/, so a recovered messages row keeps the pointer and metadata, but salvaging the DB does not decrypt the media itself. Anything physically overwritten or truncated off the end is gone — a SQLite file has no redundancy to rebuild it from. And recovered output is always suspect: because the scan reads raw cells including freelist pages, deleted messages can resurface and a value can come back as the wrong storage class, so verify against a known-good source before relying on it.
Why this belongs on your own machine
A messenger database is the single file you would least want on a server you don't control: it is every conversation, contact and group you have kept. Upload-based "repair" services copy that file onto infrastructure you can't inspect, under retention terms you didn't write — and for a Signal database, that quietly undoes the whole point of using Signal.
Here the salvage runs entirely in your browser tab: the plaintext file is read from disk, rebuilt locally, and downloaded, and you can open the Network tab and watch 0 bytes leave. Just as important, the decrypt step stays local too — your SQLCipher key comes from your own Keychain (or config.json) and is used by a tool on your machine, never transmitted. Recovering a Signal database this way means trusting no third party with either the data or the key.
What this can and can't fix
Can fix
- A Signal database you've decrypted to plaintext (via sqlcipher or a SQLCipher build of DB Browser) that then reads "database disk image is malformed"
- messages rows — the body column and the full json record — pulled from surviving b-tree leaf pages
- conversations, reactions and other tables when a single bad page or pointer took the whole file down
- Rows the pointer chain can't reach, via a raw page scan, with unattributable rows placed in a lost_and_found table
- The newest messages from a decrypted -wal sidecar, overlaid before the scan
Can't fix
- The still-encrypted db.sqlite itself — decrypt it locally first; the engine salvages plaintext SQLite, not SQLCipher ciphertext
- Anything without the key: an encryptedKey you can't unwrap (lost Keychain, a different machine or user) never becomes readable SQLite
- Message attachments — they live as separately-encrypted files under attachments.noindex/, not inside the database
- Messages physically overwritten or truncated off the end of the file — no tool can invent them
- A guarantee of correctness — deleted rows can resurface and types can shift; verify recovered data before relying on it
If a repair fails, we tell you why (missing data versus broken structure), and you are never charged for a failed repair.