IndexedDB: structured client-side storage for PWAs
Published
In one line: IndexedDB is the browser’s transactional, asynchronous database for
structured data — JavaScript objects, files, and blobs — keyed and indexed for lookup. It
is the right store for a PWA’s offline app data: far larger and richer than localStorage,
works inside service workers, and survives across sessions subject to the origin’s storage
quota.
When IndexedDB is the right tool
Section titled “When IndexedDB is the right tool”localStorageis a synchronous, string-keyed/string-valued store meant for small amounts of data — fine for a few flags, wrong for app data, and it’s not exposed to service workers.- Cache Storage stores
Request/Responsepairs for offline assets — the app shell, not your records. - IndexedDB holds structured records and binary blobs, is asynchronous (never blocks the main thread), and is reachable from both windows and service workers — the home for offline drafts, queued mutations, cached API data, and large media.
The core objects
Section titled “The core objects”- Database — named, versioned, scoped to a single origin.
- Object store — like a table; holds records (any structured-cloneable value) keyed by a key path or generated key.
- Index — a secondary lookup over a property of the stored records, so you can query by something other than the primary key.
- Transaction — every read and write happens inside one, scoped to named stores and a
mode (
readonly/readwrite). It commits automatically once its associated requests have completed and control returns to the event loop (you don’t callcommit()in normal use). - Request — each operation returns an
IDBRequestthat firesonsuccess/onerror; the result arrives on the event, not as a return value.
The asynchronous request model
Section titled “The asynchronous request model”The native API is event-based, which is its sharpest edge. A common gotcha is that a
transaction stays alive only while it has pending requests — await-ing an unrelated promise
between operations lets control return to the event loop and the transaction commit out from
under you.
const open = indexedDB.open('app', 2);
open.onupgradeneeded = (event) => { const db = event.target.result; // Schema changes are ONLY legal here, inside a versionchange transaction. if (!db.objectStoreNames.contains('drafts')) { const store = db.createObjectStore('drafts', { keyPath: 'id' }); store.createIndex('byUpdated', 'updatedAt'); }};
open.onsuccess = (event) => { const db = event.target.result; const tx = db.transaction('drafts', 'readwrite'); tx.objectStore('drafts').put({ id: 'd1', updatedAt: Date.now(), body: '…' }); tx.oncomplete = () => console.log('saved'); tx.onerror = () => console.error('write failed', tx.error);};Versioned schema upgrades
Section titled “Versioned schema upgrades”The version number is how IndexedDB does migrations. Opening with a higher version than the
stored one fires onupgradeneeded, and that callback is the only place you may create or
delete object stores and indexes. Key rules:
- Bump the version when the schema changes; branch on
event.oldVersionto apply each migration step in order. - A
versionchangeupgrade is blocked while other tabs hold the database open at the old version — listen for theblocked/versionchangeevents and close stale connections so the upgrade can proceed. - Schema work is forbidden in ordinary
readwritetransactions; attempting it throws.
Quota, persistence, and eviction
Section titled “Quota, persistence, and eviction”IndexedDB shares the single per-origin storage quota the Storage API governs, alongside
Cache Storage and others. By default storage is best-effort and can be cleared under storage
pressure, so durability-critical data should pair IndexedDB with a
navigator.storage.persist() request to ask for persistent storage. Wrap writes to catch
QuotaExceededError, then prune stale data and retry rather than failing hard. (See the
storage persistence reference for the full quota and eviction model.)
Browser & ecosystem support
Section titled “Browser & ecosystem support”IndexedDB is supported across current major browsers and is reachable from both windows and
service workers, making it the baseline durable store for offline-capable PWAs. Because the
raw event API is verbose and easy to misuse, many apps wrap it in a thin promise-based layer
— the cited web.dev guide uses the idb library, which simplifies the API while keeping the
same underlying semantics described here.
Practical checklist
Section titled “Practical checklist”- Use IndexedDB for structured/offline data; reserve
localStoragefor tiny synchronous flags. - Do all
createObjectStore/createIndexwork insideonupgradeneeded, keyed off the version. - Don’t
awaitunrelated promises mid-transaction — it lets the transaction auto-commit early. - Handle
onerroron requests and transactions; surface failures instead of dropping writes. - Define indexes for the queries you actually run, rather than scanning every record.
- Catch
QuotaExceededError, prune, and retry; callpersist()for durability-critical data. - Consider a small promise-based wrapper like
idbto avoid re-implementing the event API.