
September 08, 2026
11 min read
By Kokil Thapa | Last reviewed: September 2026
Your web app needs to store structured data locally—draft forms, cached API responses, uploaded documents, or offline queue items. JavaScript IndexedDB for client side storage is the browser-native answer when localStorage is too small and too slow. It runs asynchronously, supports indexes and transactions, and holds far more data than key-value APIs. This guide walks through opening databases, reading and writing records, migrating schemas, and pairing IndexedDB with service workers for offline apps on real production projects.
What is JavaScript IndexedDB for client side storage and when should you use it?
IndexedDB is a low-level, event-driven API built into modern browsers. It stores JavaScript objects, Blobs, and ArrayBuffers inside a per-origin database. Unlike localStorage, every operation is non-blocking. That matters on mobile devices where synchronous I/O freezes the main thread.
On a production Laravel booking app I maintain, we cache itinerary JSON and user draft selections in IndexedDB. The server remains the source of truth. IndexedDB simply keeps the UI responsive when connectivity drops on trekking routes. For similar full-stack work, see our web development services in Nepal.
Reach for IndexedDB when you need any of the following:
- Structured records with fields you want to query by index
- Storage above the ~5 MB
localStorageceiling (browsers often grant IndexedDB hundreds of MB or more) - Binary data such as PDF previews or image thumbnails
- Atomic read/write groups via transactions
- Offline-first sync queues that replay when the network returns
Skip IndexedDB for tiny flags, session tokens, or a handful of strings. Use sessionStorage or HTTP-only cookies for those cases instead.
How do you open a database and create object stores in IndexedDB?
Every IndexedDB session starts with indexedDB.open(). The call is asynchronous. You attach handlers for onsuccess, onerror, and critically onupgradeneeded. Schema changes only happen inside that upgrade event.
Step 1: Open the database
const DB_NAME = 'app-cache';
const DB_VERSION = 1;
function openAppDb() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('bookings')) {
const store = db.createObjectStore('bookings', { keyPath: 'id' });
store.createIndex('by_status', 'status', { unique: false });
store.createIndex('by_updated', 'updatedAt', { unique: false });
}
};
});
} Store your JSON payloads as plain objects. Validate them with the same rules you use server-side. A JSON formatter helps inspect cached records during development.
Step 2: Choose key strategies
You have two primary options for record keys:
- keyPath — the object must contain that field; IndexedDB uses it as the primary key
- autoIncrement — the database assigns numeric keys; useful for log-style tables
Composite lookups belong in indexes, not duplicate keys. Create an index on email, status, or createdAt depending on your query patterns.
The raw API is verbose. Many teams wrap it with the idb helper library from Jake Archibald, or use Dexie.js for richer query syntax. Both compile to the same underlying API described in the MDN IndexedDB documentation.
How do you read and write data with IndexedDB transactions?
IndexedDB groups operations into transactions scoped to one or more object stores. A transaction mode controls concurrency:
readonly— multiple readers allowed- readwrite — exclusive write access to touched stores
Transactions auto-commit when the event loop goes idle with no pending requests. Never await unrelated work inside a transaction callback. That pattern causes subtle commit failures.
Writing a record
async function saveBooking(db, booking) {
return new Promise((resolve, reject) => {
const tx = db.transaction('bookings', 'readwrite');
const store = tx.objectStore('bookings');
store.put({
id: booking.id,
status: booking.status,
payload: booking,
updatedAt: Date.now(),
});
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
} Reading by index
async function getPendingBookings(db) {
return new Promise((resolve, reject) => {
const tx = db.transaction('bookings', 'readonly');
const index = tx.objectStore('bookings').index('by_status');
const request = index.getAll('pending');
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
} Promises make the flow readable. If you prefer async/await throughout, read our notes on JavaScript async/await common pitfalls first. Mixing async gaps inside transaction callbacks is a frequent source of bugs.
On legal-tech portals with document uploads, I store metadata in IndexedDB and Blobs for unsent files. The pattern mirrors server-side queues. Files sync to object storage once connectivity returns, similar to patterns in Laravel file uploads with S3, R2, and local storage.
What is the difference between IndexedDB, localStorage, and sessionStorage?
All three APIs persist data in the browser. They solve different problems. Picking the wrong one creates performance pain or storage limits later.
| Feature | IndexedDB | localStorage | sessionStorage |
|---|---|---|---|
| Data model | Structured objects, Blobs, indexes | String key-value only | String key-value only |
| API style | Asynchronous (event-driven) | Synchronous (blocks main thread) | Synchronous |
| Typical size limit | Large (often 50%+ of disk quota) | ~5 MB per origin | ~5 MB per tab |
| Transactions | Yes — atomic read/write groups | No | No |
| Indexed queries | Yes — via keyPath and indexes | No — manual JSON parse loops | No |
| Service Worker access | Yes | Yes | No (tab-scoped) |
| Best for | Offline apps, caches, drafts, media | UI prefs, small flags | Wizard state within one tab |
localStorage looks simpler. A loop over 2,000 JSON strings to find one record will jank the UI. IndexedDB pays setup cost upfront but scales with data volume. For type-safe wrappers around async storage code, see TypeScript for JavaScript developers.
How do you handle IndexedDB versioning, migrations, and errors in production?
Schema changes require bumping DB_VERSION. The browser fires onupgradeneeded with the old and new version numbers. Add stores and indexes there. You cannot change keyPaths on an existing store—you must create a new store and migrate data manually.
Migration pattern
request.onupgradeneeded = (event) => {
const db = event.target.result;
const oldVersion = event.oldVersion;
if (oldVersion < 1) {
db.createObjectStore('bookings', { keyPath: 'id' });
}
if (oldVersion < 2) {
const store = tx.objectStore('bookings');
store.createIndex('by_user', 'userId', { unique: false });
}
if (oldVersion < 3) {
db.createObjectStore('sync_queue', { keyPath: 'id', autoIncrement: true });
}
}; Test migrations with a matrix of old versions. Clear-site-data in DevTools alone is not enough. Keep a version manifest in your app config. Log upgrade failures to your monitoring endpoint.
Common production errors
- QuotaExceededError — disk full or user denied storage; prune old cache entries
- VersionError — two tabs opened different versions simultaneously; reload the stale tab
- InvalidStateError — transaction closed before requests finished; shorten async gaps
- Private browsing edge cases — Safari may evict data aggressively; always handle
onerror
Wrap storage access in a single module. Export getDb(), cacheSet(), and cacheGet() helpers. That isolation simplifies testing and future swaps to the Cache API for static assets. Our testing and optimization services often catch storage regressions during QA passes on slow networks.
For eCommerce carts that survive refresh, IndexedDB holds line items until checkout completes. WooCommerce and custom Laravel carts on projects like Quick And Easy Nepalese Grocery still rely on server sessions for payment authority. Client storage is a convenience layer, not a ledger.
How do you debug IndexedDB and build offline-capable web apps?
Chrome DevTools exposes an Application → IndexedDB tree. Firefox and Safari offer similar panels. You can inspect object stores, edit records, and delete databases during development.
Offline sync queue example
A practical offline pattern stores failed POST payloads in a sync_queue store. When navigator.onLine flips true, a service worker or page script drains the queue.
async function enqueueSyncAction(db, action) {
const tx = db.transaction('sync_queue', 'readwrite');
tx.objectStore('sync_queue').add({
url: action.url,
method: action.method,
body: action.body,
createdAt: Date.now(),
});
return txComplete(tx);
}
async function flushSyncQueue(db) {
const items = await getAllSyncItems(db);
for (const item of items) {
const res = await fetch(item.url, {
method: item.method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item.body),
});
if (res.ok) await deleteSyncItem(db, item.id);
}
} Debounce online/offline handlers so you do not hammer the API. Our guide on JavaScript debouncing and throttling covers the timing logic.
Pair IndexedDB with the Cache API for static shells. The service worker serves cached HTML and JS. IndexedDB holds dynamic user data. Together they power offline booking flows on travel sites such as Adventure Third Pole Trek.
Security and privacy notes
IndexedDB is origin-scoped. Any script on your domain can read it. XSS therefore exposes cached data. Sanitize output, use Content Security Policy, and avoid storing secrets or PII you do not need offline.
The W3C specification defines the storage model in the IndexedDB standard. Browser vendors implement slightly different quota prompts. Always request navigator.storage.persist() when your app is installed as a PWA and must retain data long-term.
On law-firm intake forms, I cache half-filled applications locally. Users appreciate not losing work. We still POST to Laravel with CSRF protection once they submit. That split keeps Court Marriage In Nepal forms usable on unstable mobile networks without treating the browser as a database of record.
Need structured API design for sync endpoints? Our API development services cover idempotent writes and conflict resolution. For performance audits that include client storage review, see speed optimization services.
Key Takeaways
- Use JavaScript IndexedDB for client side storage when you need structured, indexed, high-volume data—not for tiny string flags.
- Create and migrate schemas only inside
onupgradeneeded; bumpDB_VERSIONfor every schema change. - Keep transactions short; never await unrelated promises inside a readwrite transaction.
- Pair IndexedDB with service workers for offline-first apps; keep the server as the authoritative source of truth.
- Handle
QuotaExceededErrorand private-mode eviction gracefully with pruning and user messaging. - Wrap the raw API in a small storage module—or use Dexie.js or idb—to reduce boilerplate and bugs.
People Also Ask
Is IndexedDB supported in all modern browsers?
Yes. Chrome, Firefox, Safari, and Edge all support IndexedDB. Mobile WebViews on current Android and iOS versions include it as well. Feature-detect with 'indexedDB' in window before opening a database. Provide a degraded experience for the rare legacy environment.
Can IndexedDB store files and images?
Yes. Store Blob or File objects directly in object stores. You can also persist ArrayBuffers. This makes IndexedDB suitable for document previews and image thumbnails. Large media libraries may still belong on the server or in the Cache API for static assets.
How much data can IndexedDB hold?
There is no fixed number in the spec. Browsers grant a portion of available disk space per origin—often hundreds of megabytes or more. Users may see a permission prompt on some platforms. Call navigator.storage.estimate() to inspect usage and quota during development.
Should I use IndexedDB or SQLite via WebAssembly?
IndexedDB is the native browser path with no extra binary download. WASM SQLite suits complex relational queries or porting existing SQL logic. For most web apps—caches, drafts, offline queues—IndexedDB is simpler and sufficient.
Ship Offline-Ready Apps with Confidence
JavaScript IndexedDB for client side storage is the right tool when your app must work without a constant network connection. Open a versioned database, define object stores with indexes, and wrap reads and writes in short transactions. Test migrations, cap cache size, and sync back to your API with idempotent endpoints.
If you are building an offline-capable portal, eCommerce flow, or custom PWA and want the storage layer done properly from day one, contact us or explore custom software development and e-commerce development services. You can also browse the portfolio for examples of production web systems that handle real user data responsibly.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

