Fix it now
- Drop the file
- Repair runs locally
- Download the result
When SQLite — or anything built on it: a browser history store, a phone backup, a messaging app's local database, a desktop app's settings — reports "database disk image is malformed", it has hit SQLITE_CORRUPT (result code 11). It was walking the b-tree that indexes your tables and landed on a page whose type byte, cell pointers or record header contradict the format, so it stops rather than hand back garbage. Drop the .db, .sqlite or .sqlite3 file above and the tool reads it page by page in your browser, follows whatever b-tree structure survives, sweeps the rest with a pointer-free scan, and writes every decodable row into a brand-new, openable database — without the file ever leaving your machine.
What "malformed" means at the page level
A SQLite database is one file cut into fixed-size pages — a power of two between 512 and 65536 bytes, stored as a 16-bit value at offset 16 of the header (the on-disk value 1 is the spec's stand-in for 65536). Every file opens with the 16-byte magic string SQLite format 3\0, followed by a 100-byte header that records the page size, the text encoding at offset 56 (1 = UTF-8, 2 = UTF-16le, 3 = UTF-16be), the in-header page count at offset 28, and the freelist. Get the page size wrong and every page after the first lands at the wrong offset — which is one way a file goes "malformed" without a single row being touched.
Inside, the data lives in b-trees. Each page starts with a type byte: 0x0d for a table leaf (which holds actual rows), 0x05 for a table interior (which only holds pointers to child pages), and 0x0a/0x02 for the index equivalents. Page 1 is special — its b-tree header sits after the 100-byte database header, and it roots sqlite_master, the schema table whose rows are (type, name, tbl_name, rootpage, sql). To read one of your tables, SQLite looks up its rootpage there, then walks interior pages down to the leaves, reading each cell's varint payload length, varint rowid and record body.
Because a query traverses this tree, one bad link poisons everything beneath it. A zeroed interior page, a cell pointer that aims past the end of the page, a right-most pointer that loops back, or a record header whose serial types do not add up — any of these makes SQLite raise "database disk image is malformed" the moment it steps on the damage. The rows on the healthy leaf pages are untouched; SQLite simply cannot reach them through a broken index. PRAGMA integrity_check reports the same class of fault.
How a raw page-walk salvages your rows
The fix mirrors what SQLite's own .recover shell command does, but runs entirely in the browser. First the tool reads sqlite_master from page 1 to learn each table's name, columns (parsed from its CREATE TABLE SQL) and rootpage, then walks each b-tree from that root — the fast, exact path when the pointers are intact. When a walk hits a missing or invalid page it degrades to "incomplete" rather than throwing, keeping every row it collected up to the break.
Then comes the part that beats a plain re-open: a pointer-free sweep. The scanner ignores all pointers and treats every table-leaf page (type 0x0d) in the file as a bag of records, decoding each cell straight from its bytes — the varint header length, the serial-type array, then each value (serial type 0 is NULL, 8/9 are the literals 0 and 1, 7 is a float64, even/odd codes above 12 are BLOB/TEXT). Overflow chains for large values are followed page to page. This is exactly how rows survive a zeroed interior page, a freelisted page, or a dropped table: the leaf still holds the data even when nothing points at it. Rows whose column count uniquely matches a known table are slotted back into it; the truly un-attributable ones are written to a lost_and_found table with pgno, cellidx, nfield, rowid and c0…cN columns, so nothing decodable is thrown away.
Finally the tool builds a fresh database with sqlite3.wasm (loaded same-origin from /engines/sqlite/), issuing a permissive CREATE TABLE for each table — original column names and any INTEGER PRIMARY KEY preserved so the rowid alias round-trips, but no NOT NULL/UNIQUE/CHECK/foreign-key constraints that would reject a resurrected row — then INSERT OR IGNOREs every recovered row and exports the result as a clean .db. If a sibling -wal write-ahead-log file is provided, its committed frames are overlaid first, so recovery reflects the last committed transaction.
Where the data genuinely stops — and why nothing is uploaded
Recovery reaches what is present; it cannot invent what the disk no longer holds. If the file was truncated — a copy or sync that stopped early, leaving a size that is not an exact multiple of the page size — every page past the cut is physically gone, and you get the rows that were fully written before it. Cells that sit on intact pages but whose record headers are self-inconsistent (classic bit-rot) are dropped and counted rather than guessed. An encrypted database (SQLCipher or the SEE extension) has no plaintext magic — its first page is ciphertext — so without the key there is nothing to walk, and recovery is not attempted.
Two honesty notes SQLite itself insists on. Deleted rows can reappear: the pointer-free scan reads freelist and un-vacuumed pages, so records you thought were gone may resurface in the output. And recovered values reflect on-disk storage classes, not the declared column affinities — a column's apparent type can shift. SQLite's own guidance is blunt: data pulled from a corrupt database is always suspect. Verify it against a known-good source before you rely on it.
All of this runs in your browser tab — the scanner is dependency-free TypeScript and the writer is WASM, so there is no server round-trip. That matters when the database is a password vault, a chat history, health data or an app's private store: you can open the Network tab and confirm 0 bytes of the file ever leave your machine.
What this can and can't fix
Can fix
- Databases that raise SQLITE_CORRUPT / "database disk image is malformed" from a damaged b-tree page or pointer
- Rows on healthy table-leaf pages that a broken interior page or bad rootpage made unreachable
- Rows on zeroed interior pages, freelisted pages or dropped tables, recovered by the pointer-free leaf scan
- Large values that spilled onto overflow-page chains, reassembled during the walk
- Uncheckpointed changes in a sibling -wal file, overlaid before scanning
Can't fix
- Rows past the cut in a truncated file — those pages are physically gone
- Cells whose record header is too corrupted to decode (bit-rot) — they are dropped, not guessed
- Encrypted SQLCipher/SEE databases without the key (the pages are ciphertext)
- The original UNIQUE/CHECK/foreign-key guarantees — recovery relaxes constraints so resurrected rows are not rejected
- A file that is 0 bytes or has no SQLite magic and no decodable leaf pages
If a repair fails, we tell you why (missing data versus broken structure), and you are never charged for a failed repair.