Introduction
Welcome to the definitive deep-dive on IndexedDB! If you’re building modern web applications that need robust offline capabilities, large-scale client-side storage, or advanced querying, IndexedDB is your go-to browser API. In this guide, I’ll walk you through everything you need to knowfrom the architectural foundations to advanced performance tricks, real-world use cases, and integration with popular libraries like Dexie.js and RxDB.
What is IndexedDB?
IndexedDB is a low-level, transactional, NoSQL database built into all major browsers. Unlike localStorage or cookies, IndexedDB lets you store vast amounts of structured data, perform rich queries, and build offline-first apps that rival native experiences. It’s asynchronous, event-driven, and designed for scale.
Why Use IndexedDB?
- Offline-first apps: Store everything locally, sync when online.
- Large data sets: Store gigabytes of data, not just kilobytes.
- Rich queries: Use indexes, cursors, and key ranges for fast lookups.
- Resilience: Survive network outages, browser restarts, and quota changes.
Comparison with Alternatives
| Feature | localStorage | Cookies | WebSQL (deprecated) | IndexedDB |
| Max Size | ~5MB | ~4KB | Varies | GBs+ |
| Structured Data | No | No | Yes | Yes |
| Transactions | No | No | Yes | Yes |
| Querying | No | No | SQL | Indexes |
| Asynchronous | No | No | Yes | Yes |
| Browser Support | All | All | Deprecated | All |
IndexedDB Architecture & Concepts
Let’s start with the core building blocks of IndexedDB.
Object Stores vs Tables
IndexedDB uses object stores instead of tables. Each object store holds records, which can be JavaScript objects, strings, or numbers. You can have multiple object stores per database, each with its own schema.
1// Creating an object store2const request = indexedDB.open('MyDB', 1);3request.onupgradeneeded = (event) => {4 const db = event.target.result;5 db.createObjectStore('customers', { keyPath: 'id' });6};
Keys, Key Paths, and Key Generators
- Key Path: Property of the object used as the primary key.
- Key Generator (autoIncrement): Automatically generates unique keys.
1// Auto-incrementing keys2const store = db.createObjectStore('orders', { autoIncrement: true });
Transactions
All operations happen inside transactions. Modes:
readonly: For reading data.readwrite: For writing data.versionchange: For schema upgrades.
1const tx = db.transaction('customers', 'readwrite');2const store = tx.objectStore('customers');
Indexes
Indexes let you search by properties other than the primary key.
1store.createIndex('email', 'email', { unique: true });
Events
onsuccess: Operation completed.onerror: Operation failed.onupgradeneeded: Schema change required.onblocked: Another tab is blocking upgrade.onversionchange: Another tab requests a version change.
Storage Limits and Quota Management
IndexedDB is powerful, but browsers enforce storage quotas to protect users.
Browser-Specific Quotas
| Browser | Approx. Limit | Notes |
| Chrome | ~80% of free disk | Per origin cap |
| Firefox | ~2GB (desktop) | Prompt at 50MB |
| Safari (iOS) | ~1GB | Stricter on mobile |
| Edge | Similar to Chrome | Enterprise policies |
| Android | Similar to Chrome | Device-dependent |
Checking Usage
Use the Storage Estimation API:
1const quota = await navigator.storage.estimate();2console.log('Total:', quota.quota, 'Used:', quota.usage);
Requesting Persistent Storage
1const granted = await navigator.storage.persist();2if (granted) {3 console.log('Persistent storage granted!');4}
Handling QuotaExceededError
1try {2 await store.add(largeData);3} catch (error) {4 if (error.name === 'QuotaExceededError') {5 // Prompt user or clean up old data6 }7}
Strategies for Large Datasets
- Compression: Use Compression Streams API or RxDB key-compression.
- Sharding: Split data across subdomains or iframes.
- Expiration: Remove old or unused records.
Creating and Structuring the Database
Opening a Database
1const request = indexedDB.open('MyDB', 1);2request.onsuccess = (event) => {3 const db = event.target.result;4};5request.onerror = (event) => {6 console.error('Failed to open DB:', event.target.error);7};
Handling Version Upgrades
1request.onupgradeneeded = (event) => {2 const db = event.target.result;3 db.createObjectStore('customers', { keyPath: 'id' });4 db.createObjectStore('orders', { autoIncrement: true });5};
Creating Object Stores and Indexes
1const store = db.createObjectStore('customers', { keyPath: 'id' });2store.createIndex('email', 'email', { unique: true });3store.createIndex('name', 'name');
Example: Customer Database
1const customerData = [2 { id: 1, name: 'Alice', email: 'alice@example.com' },3 { id: 2, name: 'Bob', email: 'bob@example.com' },4];56const tx = db.transaction('customers', 'readwrite');7const store = tx.objectStore('customers');8customerData.forEach((customer) => store.add(customer));
CRUD Operations
Adding Data
1const tx = db.transaction('customers', 'readwrite');2const store = tx.objectStore('customers');3store.add({ id: 3, name: 'Carol', email: 'carol@example.com' });
Retrieving Data
1const tx = db.transaction('customers');2const store = tx.objectStore('customers');3const request = store.get(3);4request.onsuccess = (event) => {5 console.log('Customer:', event.target.result);6};
Updating Data
1const tx = db.transaction('customers', 'readwrite');2const store = tx.objectStore('customers');3const request = store.get(3);4request.onsuccess = (event) => {5 const customer = event.target.result;6 customer.name = 'Caroline';7 store.put(customer);8};
Deleting Data
1const tx = db.transaction('customers', 'readwrite');2const store = tx.objectStore('customers');3store.delete(3);
Using Cursors
1const tx = db.transaction('customers');2const store = tx.objectStore('customers');3store.openCursor().onsuccess = (event) => {4 const cursor = event.target.result;5 if (cursor) {6 console.log(cursor.key, cursor.value);7 cursor.continue();8 }9};
Key Ranges and Cursor Direction
1const range = IDBKeyRange.bound(1, 10);2store.openCursor(range, 'prev').onsuccess = (event) => {3 // Descending order4};
Advanced Querying
Using Indexes for Fast Lookups
1const tx = db.transaction('customers');2const store = tx.objectStore('customers');3const index = store.index('email');4index.get('alice@example.com').onsuccess = (event) => {5 console.log('Found:', event.target.result);6};
Compound Indexes and Multi-Property Searches
IndexedDB doesn’t support compound indexes natively, but you can store a composite key:
1// Store composite key as 'name|email'2store.createIndex('name_email', ['name', 'email']);
Filtering and Sorting with Cursors
1index.openCursor(IDBKeyRange.lowerBound('A')).onsuccess = (event) => {2 const cursor = event.target.result;3 if (cursor) {4 // Filter and sort as needed5 cursor.continue();6 }7};
Error Handling and Transactions
Error Bubbling and Transaction Aborts
Errors bubble from requests to transactions to the database. Unhandled errors abort the transaction.
1tx.onerror = (event) => {2 console.error('Transaction error:', event.target.error);3};
Handling Version Errors and Blocked Upgrades
1request.onblocked = (event) => {2 alert('Please close other tabs to upgrade the database.');3};4db.onversionchange = (event) => {5 db.close();6 alert('A new version is available. Please reload.');7};
Ensuring Durability and Consistency
Combine related operations in a single transaction to avoid partial updates.
Handling Browser Shutdowns
Listen for onclose events and avoid relying on unload for saving data.
Performance Optimization
Transaction Scope and Concurrency
Limit transaction scope to needed object stores for concurrency.
Readonly vs Readwrite Transactions
Use readonly for reads to allow concurrent access.
Bulk Operations and Batching
Use put in loops or batch with wrapper libraries for speed.
Compression
Use Compression Streams API or RxDB key-compression for large objects.
Monitoring Usage and Quota
Log results from navigator.storage.estimate() in production dashboards.
Advanced Techniques
Sharding Data Across Subdomains/Iframes
Store data under multiple origins for massive datasets. Use postMessage for
communication.
Cross-Tab Communication and Synchronization
Use BroadcastChannel or localStorage events for sync.
Data Expiration and Eviction
Implement TTL logic and clean up old records.
Storing Binary Data
Store blobs (images, videos) directly in object stores.
1store.add({ id: 4, image: myBlob });
Integration with Libraries
Dexie.js Example
1import Dexie from 'dexie';2const db = new Dexie('MyDB');3db.version(1).stores({ customers: 'id,name,email' });4await db.customers.add({ id: 5, name: 'Dave', email: 'dave@example.com' });
RxDB Example
1import { createRxDatabase, addRxPlugin } from 'rxdb';2addRxPlugin(require('pouchdb-adapter-idb'));3const db = await createRxDatabase({ name: 'mydb', adapter: 'idb' });
Security Considerations
- Same-origin policy: Only accessible from the same domain.
- Third-party restrictions: No access in strict cookie settings.
- Incognito mode: Data is ephemeral and deleted on session end.
Real-World Use Cases
- Offline note-taking apps: Store notes locally, sync when online.
- E-commerce carts: Persist cart data across sessions.
- Media storage: Cache images/videos for offline viewing.
- Analytics/logs: Store event logs for later upload.
- PWAs: Seamless offline experience with service workers.
Troubleshooting and Debugging
Common Errors
QuotaExceededError: Storage full.VersionError: Mismatched schema version.TransactionInactiveError: Transaction ended before request.
Debugging Quota Issues
- Use dev tools to simulate low storage.
- Monitor
navigator.storage.estimate().
Handling Version Changes with Multiple Tabs
- Listen for
onblockedandonversionchangeevents.
Monitoring and Logging
- Log all IndexedDB operations and errors for diagnostics.
Full Example Application
Let’s build a simple offline-first customer manager using IndexedDB and Dexie.js.
1import Dexie from 'dexie';2const db = new Dexie('CustomerManager');3db.version(1).stores({ customers: '++id,name,email' });45// Add customer6await db.customers.add({ name: 'Eve', email: 'eve@example.com' });78// Get all customers9const allCustomers = await db.customers.toArray();1011// Update customer12const customer = await db.customers.get(1);13customer.name = 'Eva';14await db.customers.put(customer);1516// Delete customer17await db.customers.delete(1);
Best Practices and Common Pitfalls
- Schema evolution: Use version upgrades for changes.
- Transaction management: Keep transactions short and focused.
- Avoid data loss: Don’t rely on
unloadevents for saving. - Browser quirks: Test on all target browsers, especially mobile Safari.
- Error handling: Always handle
onerrorand transaction aborts.
Conclusion and Further Reading
IndexedDB is a powerhouse for modern web apps. With careful schema design, robust error handling, and performance optimizations, you can build offline-first experiences that scale. For more, check out:
Happy codingand may your apps never run out of space!