August 31, 2026 · 2 min read

IndexedDB Patterns for Client-Side Data Apps

LocalStorage caps at 5MB and blocks the thread. IndexedDB holds real datasets — if you survive its API. Versioning, bulk writes, indexes, and quota discipline.

LocalStorage caps at ~5MB, stores strings only, and blocks the main thread on every access. The moment a browser app holds a real dataset — imported files, cached results, offline state — it needs IndexedDB: asynchronous, structured, and generous (typically hundreds of MB to GBs). The catch is the API, a transaction-and-callback design from 2010 that fights every modern instinct. These patterns make it tractable.

Wrap It Once, Never Touch Raw IDB Again

Nobody should write raw IndexedDB calls in application code. Wrap the database in a tiny promise-based store module with four methods — get, put, bulkPut, query — backed by one object store per entity. The wrapper owns transaction lifetimes (the #1 source of IDB bugs: transactions auto-commit when the event loop yields, so all requests must be queued synchronously inside one tick). Application code sees async functions; the wrapper absorbs the awkwardness. Small promise wrappers (~100 lines) beat both raw IDB and heavy ORMs for most apps.

Schema Versioning Without Tears

IndexedDB versions the whole database with a single integer, and upgrades run in a special transaction. The robust pattern: one migration function per version, applied in order, each creating its stores and indexes idempotently (if (!db.objectStoreNames.contains('x'))). Never delete-and-recreate stores to "change" a schema — that destroys user data on upgrade. And keep a meta store with your own schema marker, so recent code can detect ancient databases and rebuild caches deliberately instead of crashing on missing stores.

Performance: Bulk, Index, and Mind the Quota

  • Bulk writes in one transaction. A thousand single puts take seconds; one transaction with a thousand puts takes milliseconds. Always batch imports — large-file parsing should flush to IDB in chunks, not rows.
  • Index your query paths. Unindexed queries scan the whole store. Add indexes for every lookup you actually perform (by date, by category, by job ID) — but no more, since each index slows writes.
  • Respect the quota. Browsers evict IDB data under storage pressure (least-recently-used origins go first). Request persistence via navigator.storage.persist(), show usage with navigator.storage.estimate(), and treat cached data as rebuildable: the source of truth stays the user's files or your server, never the browser alone.
  • Expire aggressively. Cached analysis results should carry timestamps and a TTL sweeper. An unbounded cache becomes a quota crisis with a delay fuse.

IndexedDB is infrastructure, not a feature — users should never know it exists. Pair it with a service-worker cache for the app shell (see offline-first patterns) and the app works fully offline: shell from cache, data from IDB, compute in workers. That trio is the entire local-first stack, and IDB is its memory.