๐Ÿณ IntuitiveFE
Login
โ† All concepts

IndexedDB & Structured Storage

โฑ๏ธ ~3-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

5-Year-Old Metaphor

โ€” The physical, real-world picture. No jargon.

๐Ÿ—‚๏ธ IndexedDB = a filing cabinet in your browser with custom tab systems.

The filing cabinet analogy

Object stores are drawers โ€” each drawer holds a specific type of document (users, orders, messages). You decide the drawer names in onupgradeneeded.

Keys are folder names. You find a folder by its exact name (key). Fast O(log n) lookup.

Indexesare custom tab systems. Without an index on "email", finding a user by email means flipping through every folder (scan). With an index, there's a separate alphabetical tab system that points directly to the right folder.

Transactions are a locked-room session. You check out folders, make changes, and check them back in atomically โ€” no one else can interfere mid-operation.

Why async when localStorage is sync?

localStorage blocks the main thread โ€” a 10MB read freezes your UI. IndexedDB runs on a background thread โ€” reads happen without blocking rendering. This is why IDB uses callbacks/IDBRequest (and modern libraries wrap it in Promises). The async overhead is worth it for large datasets.

๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.

Pattern

Open a database and define object stores in onupgradeneeded

openindexedDB.open('myApp', 1)
openonupgradeneeded: createObjectStore('users', { keyPath: 'id' })
openstore.createIndex('by-email', 'email', { unique: true })
openonupgradeneeded v2: store.createIndex('by-role', 'role')
openonsuccess: db = e.target.result
Step 1/5: Open (or create) the 'myApp' database at version 1. Returns an IDBOpenDBRequest. Fires onupgradeneeded if this is a new DB or version is higher than existing.
1 / 5
โš ๏ธ Gotcha: You cannot modify object stores or indexes outside of onupgradeneeded. To add a new store, you must increment the version number. This forces all schema changes through a migration path โ€” by design.
๐Ÿ’ก Insight: IndexedDB versioning is a controlled migration system. Version 1 = initial schema. Version 2 = add index. Version 3 = add new store. Never modify existing stores without version bump.
Completed:๐Ÿ—„๏ธโœ๏ธ๐Ÿ”๐Ÿ”’๐Ÿ“Š
๐ŸŽฏ

Challenge

Step through all 5 patterns to their final step. Understand schema, CRUD, indexes, transactions, and when to use IDB.

Try it
๐ŸŽฏ

Why Should I Care?

โ€” The exact interview question + the bug it kills.

Interview questions

Q: Why is IndexedDB asynchronous while localStorage is synchronous?

localStorage is a simple in-memory key-value store backed by disk. Reads are fast and block briefly โ€” acceptable for small data. IndexedDB is a real database engine (built on LevelDB/SQLite internally). Queries, index lookups, and large data reads take meaningful time. Running them synchronously on the main thread would cause visible jank. IDB's async model keeps the UI responsive during database operations.

Q: When should you use IndexedDB vs Cache API?

Cache API: for caching HTTP responses (HTML, CSS, JS, images) โ€” designed to work with Service Workers for offline support. Key = URL, value = Response object. IndexedDB: for structured application data (user profiles, messages, drafts, shopping cart) โ€” needs to be queried, indexed, and transactionally updated. Different tools for different purposes.

Q: How do you handle IndexedDB schema migrations?

Increment the version number in indexedDB.open(name, version). The onupgradeneeded handler receives e.oldVersion and e.newVersion. Use a switch/if chain to apply incremental migrations: if (oldVersion < 2) addIndex(), if (oldVersion < 3) addStore(). Never skip versions โ€” a user upgrading from v1 to v3 must go through v2 migrations.

Use idb in production

js
1import { openDB } from 'idb';
2ย 
3const db = await openDB('myApp', 2, {
4 upgrade(db, oldVersion) {
5 if (oldVersion < 1) {
6 const store = db.createObjectStore('users', { keyPath: 'id' });
7 store.createIndex('by-email', 'email', { unique: true });
8 }
9 if (oldVersion < 2) {
10 db.createObjectStore('orders', { keyPath: 'id' });
11 }
12 },
13});
14ย 
15// Clean async/await API:
16await db.put('users', { id: 'u1', name: 'Alice', email: 'alice@example.com' });
17const user = await db.get('users', 'u1');
18const all = await db.getAll('users');
19await db.delete('users', 'u1');
๐Ÿ”ฌ

The Deep Dive

โ€” Spec refs, engine internals, the minutiae.

Dexie.js โ€” rich query API

The idb library is a thin Promise wrapper. Dexie.js provides a full ORM-like API with rich querying:

js
1import Dexie from 'dexie';
2ย 
3const db = new Dexie('myApp');
4db.version(1).stores({
5 users: '++id, email, role', // ++ = auto-increment
6 orders: '++id, userId, status',
7});
8ย 
9// Rich queries
10const admins = await db.users
11 .where('role').equals('admin')
12 .toArray();
13ย 
14const userOrders = await db.orders
15 .where('userId').equals(userId)
16 .and(o => o.status === 'pending')
17 .toArray();

Storage quota and persistence

Browsers allocate storage quotas dynamically. Typical limits:

  • Chrome: up to 60% of available disk space, 20% per origin
  • Firefox: up to 10% of free disk space
  • Safari: 1GB by default (can request more)

Check and request persistence:

js
1// Check current usage
2const estimate = await navigator.storage.estimate();
3console.log(estimate.quota); // bytes available
4console.log(estimate.usage); // bytes used
5ย 
6// Request persistent storage (user-prompted in some browsers)
7const persisted = await navigator.storage.persist();
8// true = browser won't evict data automatically

Origin Private File System (OPFS)

OPFS (available in modern browsers) provides a sandboxed, virtual filesystem. It's faster than IndexedDB for large binary data and enables running SQLite natively in the browser using the synchronous OPFS Worker API:

js
1// Access OPFS root directory
2const root = await navigator.storage.getDirectory();
3ย 
4// Create a file
5const fileHandle = await root.getFileHandle('data.bin', { create: true });
6const writable = await fileHandle.createWritable();
7await writable.write(buffer);
8await writable.close();
9ย 
10// SQLite in a Worker via sqlite-wasm + OPFS
11// (synchronous I/O available only in Workers)
๐ŸŽค

Interview Questions

โ€” Real questions from real interviews โ€” with answers.

IDB is a real database engine that can block for meaningful time โ€” async keeps the main thread responsive.

Increment the version number โ€” onupgradeneeded fires with oldVersion and newVersion so you apply incremental migrations.

Transactions auto-commit when the microtask queue empties with no pending IDB requests โ€” an awaited fetch drains the queue.

IDB for structured app data you query/index; Cache API for HTTP response caching in service workers.

put() is an upsert (create or replace); add() fails with a ConstraintError if the key already exists.

๐ŸŽฎ

Memory Game

โ€” Quick quiz โ€” lock the concept in long-term memory.
1/4

Why does IndexedDB use an asynchronous API?