Back to issues
FRONTEND

IndexedDB: the hidden database in your browser

Learn how to use IndexedDB to build offline-first apps, cache structured data and sync with the backend efficiently.

By Thiago Saraiva6 MIN

Did you know your browser ships with a full NoSQL database? ACID transactions, indexes, gigabytes of storage, all sitting there unused. Most devs don't. I'm talking about IndexedDB.

While everyone reaches for localStorage (and suffers the 5MB ceiling and the strings-only tax), IndexedDB is right there, ready to solve problems localStorage simply can't.

Mental model: localStorage is a sticky note on your fridge. One line, one message, good luck finding it next week. IndexedDB is a full filing cabinet in a warehouse, with labeled drawers (object stores), cross-references (indexes), and a clerk who refuses to leave a drawer half-open (transactions).

Why IndexedDB and not localStorage?

StorageTypeCapacityIdeal for
localStorageKey-value (string)~5MBSimple configs, tokens
sessionStorageKey-value (string)~5MBTemporary state
IndexedDBTransactional NoSQLGB+Complex data, offline, blobs
Cache APIRequest/ResponseGB+HTTP cache (Service Worker)

Who actually uses it? (spoiler: everyone you love)

Gmail offline keeps your inbox searchable on a plane using IndexedDB. Figma caches fonts and file metadata there so the editor boots fast. Notion's offline mode, Google Docs offline, Excalidraw's autosave, WhatsApp Web's message history, all IndexedDB under the hood. If an app feels "instant" after a refresh, odds are good a filing cabinet is doing the heavy lifting.

Forget the native API, use idb

The native IndexedDB API is event-driven, callback-heavy, and verbose enough to make you question your career choices. Use idb, the tiny promise wrapper maintained by Jake Archibald (yes, the Service Worker guy from the Chrome team):

Clean and direct CRUD

Integration with React and Zustand

When NOT to use IndexedDB

Not every problem is a filing cabinet problem. Skip it when:

  • Tiny, simple config (theme, feature flag, last route). localStorage is synchronous and perfectly fine.
  • Cross-tab sync without a plan. IndexedDB doesn't notify other tabs. Pair it with BroadcastChannel or a storage event proxy, or you'll ship stale UIs.
  • Sensitive data without encryption. Any script on the origin (including a compromised dependency) can read it. Never dump raw tokens, PII, or secrets. If you must, encrypt with a key derived from a server session, and assume it's still not a vault.
  • Source of truth. It's a cache, not a database. Users clear it, browsers evict it, incognito nukes it.

Quota and eviction: the part nobody mentions

Here's the rug pull: by default, IndexedDB storage is best-effort. Under storage pressure (low disk, "clear browsing data", long inactivity on Safari), the browser can evict your entire database without asking. Your beautiful offline cache? Gone on a Tuesday.

I learned this the hard way shipping a "works offline" field-tech tool. We tested for a week, demoed to the client, then watched a tester open the app on Monday morning to find an empty IndexedDB. Safari had quietly evicted the whole thing over the weekend because the device sat idle. The fix was a one-liner most devs have never seen:

persist() asks the browser to upgrade your storage to "persistent", which survives eviction. Chrome grants it silently based on engagement heuristics (installed PWA, bookmarked, high usage). Firefox prompts the user. Safari... Safari does Safari things (more below).

FAQ

1. What's the real max size? "GB+" is the honest answer, but it's a moving target. Chrome/Edge allow up to ~60% of free disk per origin. Firefox caps at 10% of disk (up to 10GB) per group. Safari starts around 1GB and prompts the user for more. Always use navigator.storage.estimate() instead of guessing.

2. Any Safari quirks I should worry about? Yes, several. Under ITP, Safari wipes script-writable storage (IndexedDB included) after 7 days of no user interaction with the site, this is documented WebKit behavior, not a myth. Private mode has stricter limits and sometimes silently fails. Older Safari versions had infamous bugs with IDBObjectStore inside transactions. Test on real Safari, not "Responsive Mode" in Chrome.

3. Can I use IndexedDB inside a Service Worker? Yes, and it's the canonical pattern for offline sync and background fetch. Just remember the SW has its own lifecycle, so open the DB lazily per event, don't cache the connection in a module-level variable.

4. Will schema migrations break my users? They can, spectacularly. The upgrade callback runs once per version bump, and if it throws, the user is stuck with a half-migrated DB forever. Rules: always handle every oldVersion range, never assume a store exists, and never delete data in a migration without a backup path. Test upgrades from v1 to vN, not just vN-1 to vN.

5. Can I encrypt data in IndexedDB? Yes, use the Web Crypto API (crypto.subtle) to encrypt blobs before put(). The hard part isn't encryption, it's key management: where does the key live? If it's in localStorage, you've solved nothing. Derive it from a server-delivered session token, or use a passphrase via PBKDF2. Libraries like idb-keyval paired with Web Crypto make this less painful.

Key Takeaways

IndexedDB is one of the most powerful and underutilized tools in frontend development. If you're doing anything offline-first, caching structured data, or storing blobs, it's the answer. Start with the idb library, call navigator.storage.persist() early, plan your migrations, and you'll ship a user experience that feels like magic, minus the Tuesday eviction surprise.