# IndexedDB: structured client-side storage for PWAs

> How IndexedDB stores structured data and large blobs in the browser — object stores, indexes, transactions, the asynchronous request model, versioned schema upgrades, and why most apps reach for a wrapper.

**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

- **`localStorage`** is 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`/`Response` pairs 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

- **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 call `commit()` in normal use).
- **Request** — each operation returns an `IDBRequest` that fires `onsuccess` / `onerror`;
  the result arrives on the event, not as a return value.

## 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.

```js
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

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.oldVersion` to apply each
  migration step in order.
- A `versionchange` upgrade is blocked while other tabs hold the database open at the old
  version — listen for the `blocked`/`versionchange` events and close stale connections so
  the upgrade can proceed.
- Schema work is forbidden in ordinary `readwrite` transactions; attempting it throws.

## 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

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

- [ ] Use IndexedDB for structured/offline data; reserve `localStorage` for tiny synchronous flags.
- [ ] Do all `createObjectStore`/`createIndex` work inside `onupgradeneeded`, keyed off the version.
- [ ] Don't `await` unrelated promises mid-transaction — it lets the transaction auto-commit early.
- [ ] Handle `onerror` on 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; call `persist()` for durability-critical data.
- [ ] Consider a small promise-based wrapper like `idb` to avoid re-implementing the event API.