Part III β The Speed Β· Lesson 6.5
Indexed DB
In one lineAn asynchronous, transactional, indexed store for hundreds of megabytes of structured data.
Where you've seen itGmail offline. Thousands of messages, searchable, on a plane.
Diagram
STRUCTURE β a database, not a key-value bag
Database "mail" (version 3)
βββ Object store "messages" keyPath: "id"
β βββ index "by-thread" on threadId
β βββ index "by-date" on receivedAt
β βββ index "by-unread" on isUnread
β ββββββββββββββββββββββββββββββββββββββββ
β β {id:"m1", threadId:"t9", subject:β¦} β structured objects,
β β {id:"m2", threadId:"t9", β¦} β not strings
β ββββββββββββββββββββββββββββββββββββββββ
βββ Object store "attachments" Blobs, Files, ArrayBuffers
βββ Object store "outbox" queued sends (see 9.1)ASYNC + TRANSACTIONAL β never blocks the main thread
main thread ββ[render]ββ[render]ββ[render]ββ[render]βββΊ
β β²
db.put(msg) βββ β resolves later
βΌ β
IDB thread [ transaction ββ write βββ ]
atomic: all writes commit, or none doVERSION UPGRADE β the one-time schema hook
open('mail', 3)
β
ββ existing version 3? βββΊ ready, no upgrade
ββ version 2 on disk? βββΊ onupgradeneeded fires
create new stores/indexes
migrate or delete old data
β the ONLY place schema changesHow it works
- Open the database with a version number; schema changes happen only inside
onupgradeneeded. - Define object stores with a
keyPathand add indexes for every field you'll query by β without one you're scanning. - All reads and writes run inside a transaction, which is atomic and auto-commits when the microtask queue drains.
- Store real objects β nested structures,
Blobs,Files,ArrayBuffers β no serialisation needed. - Use a wrapper. The raw API is event-based and verbose;
idb(β1KB) makes it promise-based and readable.
import { openDB } from 'idb';
const db = await openDB('mail', 3, {
upgrade(db, oldVersion) {
if (oldVersion < 1) {
const s = db.createObjectStore('messages', { keyPath: 'id' });
s.createIndex('by-thread', 'threadId');
}
if (oldVersion < 3) db.createObjectStore('outbox', { keyPath: 'id' });
},
});
await db.put('messages', message);
const thread = await db.getAllFromIndex('messages', 'by-thread', 't9');Trade-offs
| Strength | Cost |
|---|---|
| GBs of quota, async, transactional | Verbose raw API |
| Indexed queries, cursors, ranges | No SQL, no joins β you model manually |
| Stores binary data directly | Schema migrations you must write |
| Works in workers and service workers | Evictable under storage pressure |
Quota and eviction: browsers grant a share of free disk and may evict under pressure. Call navigator.storage.persist() for critical data, and always be able to re-fetch β IndexedDB is a cache, not a database of record.
Interview angle
"How do you make a mail client work offline?"
- IndexedDB for messages with indexes on thread and date, plus an
outboxstore for queued sends. - Service worker (9.1) serves the shell and static assets; the data layer reads IDB first, then revalidates.
- Sync is the real design: a cursor/
updatedAtwatermark, last-write-wins or explicit conflict UI, and replay of the outbox on reconnect.
Recap
- Async, transactional, indexed, gigabytes β the only serious client-side store.
- Schema changes live in
onupgradeneededand nowhere else. - Use a promise wrapper, index every query field, and treat it as evictable.