ss
Navigate back to the homepage

Spaceout

beyond excelsior

Software development, application and system architecture

about meContact
Link to $https://www.facebook.com/spaceout/Link to $https://twitter.com/spaceoutLink to $https://www.instagram.com/spaceout/Link to $https://blog.spaceout.pl/Link to $https://dribbble.com/spaceoutLink to $https://behance.com/spaceoutLink to $https://github.com/massivDash/

IndexedDB The Definitive Deep-Dive Guide for Modern Web Applications

by
Luke Celitan
category: post, reading time: 3 min

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

FeaturelocalStorageCookiesWebSQL (deprecated)IndexedDB
Max Size~5MB~4KBVariesGBs+
Structured DataNoNoYesYes
TransactionsNoNoYesYes
QueryingNoNoSQLIndexes
AsynchronousNoNoYesYes
Browser SupportAllAllDeprecatedAll

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 store
2const 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 keys
2const 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

BrowserApprox. LimitNotes
Chrome~80% of free diskPer origin cap
Firefox~2GB (desktop)Prompt at 50MB
Safari (iOS)~1GBStricter on mobile
EdgeSimilar to ChromeEnterprise policies
AndroidSimilar to ChromeDevice-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 data
6 }
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];
5
6const 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 order
4};

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 needed
5 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 onblocked and onversionchange events.

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' });
4
5// Add customer
6await db.customers.add({ name: 'Eve', email: 'eve@example.com' });
7
8// Get all customers
9const allCustomers = await db.customers.toArray();
10
11// Update customer
12const customer = await db.customers.get(1);
13customer.name = 'Eva';
14await db.customers.put(customer);
15
16// Delete customer
17await 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 unload events for saving.
  • Browser quirks: Test on all target browsers, especially mobile Safari.
  • Error handling: Always handle onerror and 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:

  • IndexedDB API Reference
  • Dexie.js Documentation
  • RxDB Documentation
  • PouchDB
  • Storage Quota Management

Happy codingand may your apps never run out of space!

ss

Domain-Driven Design with TypeScript

A deep-dive into Domain-Driven Design (DDD) principles, patterns, and best practices, illustrated with TypeScript examples. Learn how to model complex domains, structure your code, and build maintainable, scalable systems.

4 min czytania

Progressive Web Apps, Offline Capabilities and Background Operations

A comprehensive, hands-on guide to building Progressive Web Apps (PWAs) with robust offline capabilities, advanced caching strategies, background sync, background fetch, push notifications, and real-world troubleshooting. Learn the architecture, implementation, performance optimization, and best practices for resilient, user-friendly web apps.

5 min czytania

Loading search index...