LevelDB / IndexedDB

What Is LevelDB, and Why Is It in Your Case?

LevelDB is a key and value store written by Google. It has no tables, no columns and no schema. It holds keys and values, both of them arbitrary bytes, and it is the storage engine underneath Chrome.

It reaches your case through Electron. Every Electron application ships a copy of Chrome, so Discord, Slack, Signal Desktop, Microsoft Teams and Notion all write their local data into LevelDB folders on the user's disk.

You do not find a file. You find a folder full of files with names such as 000123.ldb, MANIFEST-000045 and CURRENT. This page explains what those files are, why deleted records survive inside them, and why two tools can read the same folder and disagree.

The folder

What are the files in a LevelDB folder?

Six kinds, and each answers a different question. Knowing which is which is the first skill, because several of them are routinely ignored by tools that only read the current data.

FileWhat it isWhy it matters
*.ldb *.sst Table files. Sorted, immutable blocks of records. The bulk of the data. Never edited once written.
*.log The write-ahead log. Recent changes not yet merged into a table file. The newest activity lives here, and often only here.
MANIFEST-* The catalog. Lists which table files make up the database and their key ranges. Tells you which files the database considers current, and by omission which it does not.
CURRENT A one line file naming the active MANIFEST. Without it you cannot tell which catalog is authoritative.
LOCK A lock held by the process using the database. Its presence tells you the application was running.
LOG LOG.old Human readable diagnostic logs. Not evidence records. Records compaction activity, which is context for what was merged away and when.

A table file identifies itself at the end rather than the start. The last bytes of the file are a fixed trailer holding pointers to the file's own index and a magic signature.

Table file trailer signature
57 fb 80 8b 24 75 47 db at the end of every valid .ldb / .sst

If that signature is absent the file is truncated, was never a complete table, or is damaged. A careful parser skips it and says so. A careless one reads whatever it finds and presents the result as data.

Recovery

Why is a deleted record still there?

Because LevelDB never edits anything in place. It is a log-structured merge tree, which means every change is an append. Update a key and the old value is not overwritten, a new entry is added with a higher sequence number.

Deletion works the same way. LevelDB does not remove the record. It writes a new entry for the same key carrying a type byte that marks it as removed. That entry is called a tombstone.

The type byte
0x00 tombstone the key was deleted
0x01 value the key was written

Both entries now sit on disk. The original value, and the tombstone that supersedes it. Reading the database normally gives you nothing for that key, because the tombstone wins. Reading the files gives you both.

They stay until a compaction merges the files they live in. Compaction is what finally discards the old value, and it runs on the database's own schedule rather than on the user's intent. A record deleted a month ago can still be present. A record deleted an hour ago can already be gone.

Other app-data formats reach the same result by a different route. Realm releases the space a deleted object used without clearing it, and Realm database forensics covers how that plays out.

What this means in a report

A tombstone is positive evidence of a deletion, not an inference from a gap. You can state that a key held a value, that the value was later deleted, and give the sequence numbers that establish the order.

Every version survives

Edit a message five times and five entries exist until compaction merges them.

Order is explicit

Sequence numbers establish which entry came first, without relying on timestamps.

Compaction is the clock

How much history you get depends on when the database last merged, not on the case.

Nothing is carved

These are structured records read from their own format, not fragments recovered from slack.

Overlooked files

Which files hold data the database has forgotten?

The MANIFEST names the table files the database currently uses. Files that are not named in it can still be sitting in the folder, holding readable records, because deleting a file and forgetting a file are two different events.

That gives every file in the folder a role, and the roles are not interchangeable.

RoleWhat it means
Active tableNamed by the current MANIFEST. Part of the live database.
Obsolete tableSuperseded by a compaction, still present on disk.
Unreferenced tablePresent in the folder and named by nothing. The database has no idea it exists.
Current logThe write-ahead log in use at acquisition.
Previous logA log that was rotated out but not removed.
Recovery logWritten during a recovery pass after an unclean shutdown.
Old logAn earlier log left behind in the folder.

An unreferenced table file is the LevelDB parallel of a freelist page in SQLite. The engine has released it, the operating system has not reclaimed it, and the records inside are whole. The same principle appears across formats, and it is covered in the SQLite context on SQLite Forensic Analysis.

The question worth asking a tool

Did it read the whole folder, or only the files the MANIFEST pointed at? A tool that opens the database through the LevelDB library sees the current view by design, because that is what the library is for. It will not show you an obsolete table file, and it will not tell you one was there.

The layer above

Where does each browser store IndexedDB?

IndexedDB is not a file format. It is a browser API that web pages and Electron applications call to store structured data locally. What sits underneath it is the browser engine's own choice, and the three engines chose differently.

EngineStorageValue format
Chromium
Chrome, Edge, Electron apps
LevelDB folder V8 structured clone
Gecko
Firefox
SQLite database SpiderMonkey structured clone
WebKit
Safari, iOS
SQLite database WebKit SerializedScriptValue

This table explains a great deal of confusion in practice. Two examiners can both say they handled IndexedDB and mean entirely different work. One opened a folder of table files. The other opened a SQLite database and then had to decode a binary blob column that is not readable text.

It also means the SQLite skills carry directly. For Firefox and Safari the container is a SQLite database, so freelist pages, unallocated space and the write-ahead log all apply exactly as they do anywhere else. The relevant mechanics are on SQLite Forensic Analysis and Write-Ahead Log Viewer.

Key encoding

How is a Chromium IndexedDB key built?

When Chromium stores IndexedDB in LevelDB, it has to fit a structured database into a flat key and value store. It does that by encoding the structure into the key itself.

Every key begins with a prefix of four parts: a single size byte, then a database id, an object store id and an index id. The size byte does not hold data. It holds the widths of the three ids that follow.

Key prefix
[size] [db_id] [object_store_id] [index_id] [ remainder ] size = ((db_size-1) << 5) | ((os_size-1) << 2) | (idx_size-1) db 1-8 bytes, store 1-4 bytes, index 1-4 bytes, little-endian

The index id then decides what the rest of the key means. It is a routing value, not a number you read.

ConditionThe key holds
database id is 0Global metadata, including the database name map
object store id is 0Database metadata, including the origin and store definitions
index id is 1Object store data. The records themselves.
index id is 2An exists entry, used to test presence
index id is 3A blob entry, pointing at externally stored bytes
index id is 30 or aboveIndex data for a secondary index

Without decoding the prefix, every key in the folder looks like binary noise, and the metadata that names the database and its object stores is indistinguishable from the records. Decoding it turns the folder into a readable structure: this origin, this database, this object store, these records.

Where this is documented

The encoding is defined in Chromium's own source, in content/browser/indexed_db/indexed_db_leveldb_coding.cc. It is public, it is stable, and it is the reference any parser should be checked against.

Value encoding

Why is the value not JSON?

Because IndexedDB stores JavaScript objects, and JSON cannot hold everything JavaScript can. A JSON dump would lose a Date, a Map, a Set, a typed array, a BigInt and a Blob reference. So browsers use structured clone instead, a binary format that preserves them.

A structured clone value is a stream of single byte tags, each followed by its data. There is no text to search for and no delimiter to split on. You read it or you do not.

Chromium, the V8 format

A Chromium IndexedDB value is two nested envelopes. Blink writes its own version marker, then V8's serializer writes another, and the tag stream follows.

Chromium value layout
FF varint Blink envelope, wire version
FF varint V8 serializer version
[ tag stream ]

Blobs and files are a separate problem. The value does not contain the file. It contains a reference to a journal id, and the bytes live elsewhere on disk. Decoding the value tells you a file was attached. Producing the file is a second step.

Firefox and Safari

Both store their IndexedDB in SQLite, and both use a different structured clone dialect. Firefox uses SpiderMonkey's format, a stream of 64 bit pairs where the high half is a tag and the low half is data, except when the value is a float and the whole 64 bits are the number. Safari and iOS use WebKit SerializedScriptValue, a byte tag stream with its own version header.

Three engines, three formats, one API name. A tool that decodes one of them supports IndexedDB. It does not support your case unless it decodes the one in front of you. Nested binary payloads inside those values are their own problem, and Protobuf and varint forensics covers the most common one.

Validation

Why do two tools disagree about the same folder?

Run two tools over one LevelDB folder and you can get two different record counts. Neither has to be broken. They answered different questions, and usually neither of them said which question it answered.

  1. Which files were read

    One tool followed the MANIFEST and read the active database. The other read every file in the folder, including obsolete and unreferenced table files. The second finds more, and some of what it finds is historical.

  2. Whether tombstones were applied

    One tool resolved each key to its current value and dropped the deleted ones. The other listed every entry it found. One list is shorter and correct. The other is longer and correct.

  3. Whether duplicates were merged

    The same record can appear in a log file and again in a table file after a merge. Collapsing those into one row loses the fact that two independent copies exist in two places.

  4. How deep the values were decoded

    One tool showed the value as a hex blob. Another decoded the structured clone into fields. A search for a name finds nothing in the first and a hit in the second, on identical bytes.

The habit worth forming

Record counts are not comparable between tools unless you know what each one counted. Before you reconcile two outputs, establish what each read, what each skipped, and whether either applied deletions. A tool that cannot tell you is a tool you cannot reconcile. The wider argument is in why a viewer is not enough.

The workspace

Where does Elusive Data Firefly fit?

Firefly answers the two questions separately instead of blending them, because they are different questions and blending them is where disagreement comes from.

Physical history lists every occurrence found in the folder, with its source file, its position inside that file and its sequence number. Byte-identical entries in two different files stay as two rows. Nothing is deduplicated, because a second copy in a second file is a fact about the evidence.

Logical current reconstructs one row per key, applying tombstones and sequence order to say what the value is now. It reports one of three states, and the third one is the important one.

Logical state
Current value the key holds this value
Currently deleted a tombstone supersedes it
Indeterminate the evidence does not settle it

Indeterminate appears when the database's own bookkeeping cannot be trusted, for example when the MANIFEST was only partly captured. Firefly says so and sends you to the physical history rather than picking the answer that looks most likely.

Coverage is stated, not implied

A parse finishes as Complete or Partial, and when it is Partial the reason is named. Each issue is written in examiner language with what happened, what it means for the evidence and a likely cause, followed by a stable code, the byte offset, how many bytes went unread and how many records were skipped.

File roles classified

Active, obsolete and unreferenced table files, and current, previous, recovery and old logs.

Three value formats

V8, SpiderMonkey and WebKit structured clone, so Chromium, Firefox and Safari all decode.

Identity per occurrence

Each physical record gets a SHA-256 identity built from its file, location and sequence.

No invented offsets

Inside a compressed block, the block offset and the entry offset are reported separately.

Training

Where do you learn the method?

The method transfers. Identify the container, read the record header, know what marks a deletion, and establish what the tool read before you rely on what it showed. Two of the three IndexedDB engines store their data in SQLite, so the foundation is the same one.

Complete · 24 CPE · Certificate

Certified SQLite Forensics

The foundation everything else stands on, including the SQLite databases that hold Firefox and Safari IndexedDB. Taught on real training files you work on in your own tools.

  • Read a record header and know what each field is doing
  • Recover deleted content and name the mechanism that preserved it
  • Decode nested binary payloads instead of reporting them as blobs
  • Verify a timestamp interpretation instead of accepting a conversion
  • Validate incomplete or unsupported tool output against the source
  • Explain how a finding was reached, in terms a reviewer can follow
Focused · 5 CPE · On demand

WAL Frames and SHM Index

A shorter course on transaction files. Write-ahead logging is the same idea LevelDB uses, applied to SQLite, and it is a good first step if you want to see how the training works.

  • Read a WAL frame header and find transaction boundaries
  • Recover page images written before the last checkpoint
  • Handle a file set without checkpointing away the evidence

Certified SQLite Forensics also runs live online and on-site by arrangement. See the formats.

Last updated 7 September 2026