Going Beyond CRUD
Module overview
Section titled “Module overview”Basic IndexedDB CRUD — put, get, delete — gets you a long way, but real applications need more. This module covers the advanced layer that sits on top of the core API.
| Lesson | What you will learn |
|---|---|
| 1. This overview | How indexes, cursors, key ranges, the idb library, migrations, and blobs fit together |
| 2. Indexes | Creating and querying secondary indexes; index.get vs index.getAll |
| 3. Cursors & key ranges | Walking ordered results; IDBKeyRange.bound, lowerBound, upperBound |
| 4. The idb library | Promise-based wrapper; why you should almost always use it in production |
| 5. Migrations | onupgradeneeded patterns for safe schema evolution across versions |
| 6. Blobs & files | Storing binary data — images, audio, PDFs — directly in an object store |
What is an index?
Section titled “What is an index?”An object store is like a table keyed on a primary key (usually id). An index is a secondary sorted view of the same records keyed on a different property — for example email or createdAt.
Without an index, finding all records where email === '[email protected]' requires opening a cursor over every record (a full scan). With an index on email, the engine jumps straight to the matching entries in O(log n).
// Creating a store with an index during onupgradeneededconst store = db.createObjectStore('users', { keyPath: 'id' });store.createIndex('by_email', 'email', { unique: true });store.createIndex('by_age', 'age', { unique: false });Once the index exists you query it inside any transaction:
const tx = db.transaction('users', 'readonly');const index = tx.objectStore('users').index('by_email');Cursors let you walk results
Section titled “Cursors let you walk results”A cursor iterates records in index order. You open it on either the object store (primary key order) or an index (secondary key order), then call cursor.continue() to advance:
const tx = db.transaction('users', 'readonly');const index = tx.objectStore('users').index('by_age');const range = IDBKeyRange.bound(18, 65); // ages 18–65 inclusivelet cursor = await index.openCursor(range);
while (cursor) { console.log(cursor.value.name, cursor.value.age); cursor = await cursor.continue();}Key ranges — bound, lowerBound, upperBound, only — can be passed to both openCursor and getAll to filter without a full scan.
The idb library
Section titled “The idb library”The raw IndexedDB API is callback/event-driven and verbose. Jake Archibald’s idb library wraps it in clean Promises with no runtime overhead:
import { openDB } from 'idb';
const db = await openDB('my-app', 1, { upgrade(db) { const store = db.createObjectStore('users', { keyPath: 'id' }); store.createIndex('by_email', 'email', { unique: true }); },});
The module’s later lessons use both raw IDB (to understand the primitives) and idb (for realistic production patterns).
Migrations with onupgradeneeded
Section titled “Migrations with onupgradeneeded”Every time you increment the version number passed to indexedDB.open, the browser fires onupgradeneeded before the database opens. This is the only place where you can create or delete object stores and indexes.
const db = await openDB('app', 3, { upgrade(db, oldVersion) { if (oldVersion < 1) { db.createObjectStore('users', { keyPath: 'id' }); } if (oldVersion < 2) { db.transaction.objectStore('users') .createIndex('by_email', 'email', { unique: true }); } if (oldVersion < 3) { db.createObjectStore('files', { keyPath: 'name' }); } },});Chained if (oldVersion < N) blocks let users upgrade from any older version in a single pass — the key migration pattern covered in depth in lesson 5.
Blobs and files
Section titled “Blobs and files”IndexedDB stores structured-cloneable values, which includes Blob, File, ArrayBuffer, and ImageData. There is no need to base64-encode binary data the way you would for localStorage:
const response = await fetch('/avatar.png');const blob = await response.blob();
await db.put('files', { name: 'avatar.png', data: blob, size: blob.size });
const record = await db.get('files', 'avatar.png');const url = URL.createObjectURL(record.data);// use url in an <img> srcStoring blobs is covered end-to-end in lesson 6.
Object store → index relationship
Section titled “Object store → index relationship”flowchart LR
subgraph ObjectStore["Object Store — users"]
direction TB
R1["{ id:1, name:'Ada', email:'[email protected]', age:36 }"]
R2["{ id:2, name:'Bob', email:'[email protected]', age:24 }"]
R3["{ id:3, name:'Carol', email:'[email protected]', age:31 }"]
end
subgraph PK["Primary Key (id)"]
direction TB
PK1["1 → record 1"]
PK2["2 → record 2"]
PK3["3 → record 3"]
end
subgraph IDX["Index — by_email"]
direction TB
I1["[email protected] → record 1"]
I2["[email protected] → record 2"]
I3["[email protected] → record 3"]
end
ObjectStore --> PK
ObjectStore --> IDX
IDX -. "index.get('[email protected]')" .-> R1