Digital Thrive
Modern web applications increasingly require robust client-side storage solutions to deliver seamless user experiences, especially in offline or unreliable network environments. IndexedDB stands out as a powerful, transactional, and reliable client-side NoSQL database that enables web applications to store significant amounts of structured data directly within the user's browser. Unlike simple key-value storage solutions such as localStorage, which is limited to string key-value pairs with a typical storage limit of around 5-10MB, IndexedDB provides a much more powerful solution for applications requiring larger data storage capabilities. The database operates asynchronously, meaning operations don't block the main thread, and supports transactional operations that ensure data integrity even when multiple operations are performed simultaneously, as documented in the [MDN Web Docs IndexedDB guide](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB). The evolution of web applications toward Progressive Web Apps (PWAs) and offline-first architectures has made IndexedDB an essential technology for developers building sophisticated browser-based applications. Whether you're developing a productivity tool that needs to store user documents, a music streaming application that caches audio files locally, or a complex form wizard that preserves user progress across sessions, IndexedDB provides the foundation for persistent client-side data management that rivals native application capabilities. This makes it particularly valuable for our [web development services](/services/web-development/) where we build applications that must work reliably across all conditions. This comprehensive guide walks you through everything you need to know to effectively use IndexedDB in your web projects. We start with the fundamentals of database setup and object store creation, progress through transactional operations and data manipulation, explore advanced querying techniques with cursors and indexes, and culminate in a complete real-world implementation example. For developers working with state management libraries alongside IndexedDB, our guide on [using Redux Toolkit's createAsyncThunk](/resources/guides/web-design/using-redux-toolkits-createasyncthunk/) demonstrates how to integrate async data operations into your Redux workflow. By the end of this guide, you'll have the knowledge and practical skills to implement robust client-side storage solutions using IndexedDB in any web application.
IndexedDB is a low-level API for client-side storage of significant amounts of structured data, including files and blobs. Unlike localStorage, which is limited to string key-value pairs with a typical storage limit of around 5-10MB, IndexedDB provides a much more powerful solution for applications requiring larger data storage capabilities. The database operates asynchronously, meaning operations don't block the main thread, and supports transactional operations that ensure data integrity even when multiple operations are performed simultaneously. One of the most significant advantages of IndexedDB is its support for indexing. You can create indexes on object store properties, enabling efficient querying and retrieval operations that would be prohibitively slow with manual iteration. For example, if you have an object store containing user records, you can create an index on the email property to quickly look up users by their email addresses without scanning through every record. This indexing capability transforms IndexedDB from a simple storage solution into a full-featured client-side database that can handle complex data retrieval patterns common in application development. The transactional nature of IndexedDB operations provides ACID (Atomicity, Consistency, Isolation, Durability) guarantees that are essential for applications where data integrity matters. When you perform multiple operations as part of a transaction, either all operations complete successfully, or none of them take effect. This behavior prevents partial updates that could leave your data in an inconsistent state, making IndexedDB suitable for applications handling important user data such as form submissions, shopping carts, or configuration settings. IndexedDB is supported across all major modern browsers, including Chrome, Firefox, Safari, Edge, and Opera. The API has been standardized by the W3C and is considered a stable, mature technology for web application development. This broad browser support means you can confidently build applications using IndexedDB knowing that the vast majority of your users will be able to take advantage of its capabilities.
The first step in working with IndexedDB is opening a connection to a database. The `indexedDB.open()` method initiates this connection and returns an IDBOpenDBRequest object that you use to handle success and error events. The method takes two parameters: the database name and an optional version number. If the database doesn't exist, opening it creates a new database with the specified version. If the database exists and the version number matches the current version, a connection is established without triggering any upgrade events. When opening a database with a version number higher than the existing database version, or when opening a database that doesn't exist, the `onupgradeneeded` event is triggered. This event provides the opportunity to create or modify the database schema, including creating object stores and indexes. This upgrade pattern ensures that the database schema is always in sync with your application's requirements while maintaining backward compatibility with existing user data. The version number used when opening a database must be a positive integer. Using decimal values or invalid version numbers will result in errors. When incrementing the version number, you're essentially telling IndexedDB that you want to modify the database structure, which triggers the upgrade process. This version-based upgrade mechanism is the standard way to handle database migrations in IndexedDB, allowing you to safely evolve your data model over time. Proper version management is crucial for maintaining database integrity and enabling smooth upgrades without losing user data.
1const DB_NAME = 'TaskManagerDB';2const DB_VERSION = 1;3 4let db;5 6function openDatabase() {7 return new Promise((resolve, reject) => {8 // Open database with version number9 const request = indexedDB.open(DB_NAME, DB_VERSION);10 11 // Handle successful database opening12 request.onsuccess = (event) => {13 db = event.target.result;14 console.log('Database opened successfully');15 resolve(db);16 };17 18 // Handle errors19 request.onerror = (event) => {20 console.error('Database error:', event.target.error);21 reject(event.target.error);22 };23 24 // Handle database upgrade (schema changes)25 request.onupgradeneeded = (event) => {26 const database = event.target.result;27 28 // Create object store with keyPath29 if (!database.objectStoreNames.contains('tasks')) {30 const taskStore = database.createObjectStore('tasks', { keyPath: 'id' });31 // Create index for querying32 taskStore.createIndex('status', 'status', { unique: false });33 taskStore.createIndex('dueDate', 'dueDate', { unique: false });34 }35 };36 });37}38 39// Usage40async function initDatabase() {41 try {42 await openDatabase();43 console.log('Ready to use IndexedDB');44 } catch (error) {45 console.error('Failed to open database:', error);46 }47}Object stores are the containers within an IndexedDB database that hold your data records. Each object store functions similarly to a table in a traditional database, but unlike tables, object stores are designed to hold objects rather than rows of scalar values. When you create an object store, you specify a key path or key generator that uniquely identifies each record in the store. The key path is a property name that serves as the primary key for records, while autoIncrement enables automatic key generation for records that don't explicitly specify a key. When choosing between explicit key paths and auto-incrementing keys, consider your data model and access patterns. Explicit key paths work well when you have natural identifiers in your data, such as email addresses, UUIDs, or user-specified identifiers. Auto-incrementing keys are convenient when records don't have natural unique identifiers or when you prefer to let the database generate keys automatically. You can also create object stores without a key path and use out-of-line keys by specifying a separate key generator, though this pattern is less common. Once created, object stores cannot be deleted or modified within the same version upgrade. To change an object store's configuration, you would typically need to create a new object store with the desired configuration and migrate data from the old store. This limitation is by design—it prevents accidental schema modifications that could lead to data loss. Planning your schema carefully during initial development saves significant effort later when you need to migrate data structures. This is especially important when building complex applications that may require [performance optimization](/services/web-development/) as your data needs evolve. Indexes in IndexedDB function similarly to indexes in traditional databases—they provide efficient lookup capabilities based on specific record properties. Without indexes, retrieving records by properties other than the key requires scanning through all records in the object store, which becomes increasingly slow as the dataset grows. By creating indexes on frequently queried properties, you enable IndexedDB to quickly locate relevant records without full store scans.
| keyPath | string | array | Property name(s) that uniquely identify each object. Required for object stores. |
| autoIncrement | boolean | If true, automatically generates unique numeric keys. Useful when objects don't have natural keys. |
| name | string | Logical name of the object store. Used in transactions and queries. |
| Indexes | array | Additional indexes created on specific properties for faster queries. |
Transactions are the fundamental mechanism for ensuring data integrity in IndexedDB. Every operation that reads or writes data occurs within the context of a transaction, which groups operations into atomic units. If any operation within a transaction fails, all changes made during that transaction are rolled back, leaving the database in its previous consistent state. This behavior is essential for maintaining data integrity, especially when multiple related operations must succeed or fail together. IndexedDB transactions have two critical characteristics that developers must understand. First, transactions operate in a specific mode that determines what operations are permitted. The three modes are `readonly`, `readwrite`, and `versionchange`. Readonly transactions can only read data, readwrite transactions can read and write, and versionchange transactions can modify the database schema. Second, transactions have a limited lifespan—they automatically become inactive if the application doesn't complete operations within a reasonable timeframe. Creating a transaction requires specifying which object stores the transaction will access and the mode in which it operates. You can access multiple object stores within a single transaction, which is useful when operations need to span multiple stores atomically. The transaction object provides access to the individual object stores through its `objectStore()` method, and you can attach event listeners for transaction-level events such as completion and error. One important aspect of IndexedDB transactions is that they automatically become inactive if the transaction's callback function completes without all requests completing. This design prevents long-running transactions from blocking other operations. However, it means you must keep transactions alive by queuing operations and waiting for their results. If you need to perform complex multi-step operations, you'll need to structure your code using nested callbacks, promises, or async/await patterns to maintain the transaction until all operations complete. For TypeScript projects, our guide on [using TypeScript with Redux Toolkit](/resources/guides/web-design/using-typescript-redux-toolkit/) covers type-safe async patterns that work well with IndexedDB operations.
1// Transaction modes2const transactionModes = {3 readonly: 'readonly', // Read data only4 readwrite: 'readwrite', // Read and write data5 versionchange: 'versionchange' // Modify schema (auto-used in onupgradeneeded)6};7 8function performTransaction(storeName, mode, operation) {9 return new Promise((resolve, reject) => {10 // Start transaction with specified mode and scope11 const transaction = db.transaction(storeName, mode);12 13 // Get object store14 const store = transaction.objectStore(storeName);15 16 // Handle transaction completion17 transaction.oncomplete = () => {18 resolve('Transaction completed successfully');19 };20 21 // Handle transaction errors22 transaction.onerror = (event) => {23 console.error('Transaction error:', event.target.error);24 reject(event.target.error);25 };26 27 // Handle transaction abort28 transaction.onabort = () => {29 console.warn('Transaction was aborted');30 reject(new Error('Transaction aborted'));31 };32 33 try {34 operation(store);35 } catch (error) {36 transaction.abort();37 reject(error);38 }39 });40}41 42// Usage example - Readonly transaction43async function getAllTasks() {44 return performTransaction('tasks', 'readonly', (store) => {45 const request = store.getAll();46 request.onsuccess = () => resolve(request.result);47 request.onerror = () => reject(request.error);48 });49}All IndexedDB operations follow the same fundamental pattern: open a transaction, get the object store, perform the operation, and handle the result. This consistent pattern makes it easier to learn and implement the four core operations: Create (add), Read (get), Update (put), and Delete (delete). Understanding this pattern is essential for building reliable data management in your applications. The `add()` method creates a new record and fails if a record with the same key already exists, while `put()` creates a new record or updates an existing record with the same key. This distinction makes `add()` suitable for creating new records where you want to ensure no duplicates, and `put()` ideal for upsert operations where you want to create or update as needed. For reading data, you can retrieve records by their primary key using `get()`, or use indexes to query records by other properties. The `delete()` method removes records permanently from the object store. Each operation returns a request object that fires success or error events when the operation completes.
1// CREATE - Add new records2async function addTask(task) {3 return new Promise((resolve, reject) => {4 const transaction = db.transaction(['tasks'], 'readwrite');5 const store = transaction.objectStore('tasks');6 7 const request = store.add(task);8 9 request.onsuccess = () => {10 console.log('Task added with key:', request.result);11 resolve(request.result);12 };13 14 request.onerror = () => {15 console.error('Error adding task:', request.error);16 reject(request.error);17 };18 });19}20 21// READ - Get single record by key22async function getTask(id) {23 return new Promise((resolve, reject) => {24 const transaction = db.transaction(['tasks'], 'readonly');25 const store = transaction.objectStore('tasks');26 27 const request = store.get(id);28 29 request.onsuccess = () => {30 resolve(request.result);31 };32 33 request.onerror = () => {34 reject(request.error);35 };36 });37}38 39// READ - Get all records40async function getAllTasks() {41 return new Promise((resolve, reject) => {42 const transaction = db.transaction(['tasks'], 'readonly');43 const store = transaction.objectStore('tasks');44 45 const request = store.getAll();46 47 request.onsuccess = () => {48 resolve(request.result);49 };50 51 request.onerror = () => {52 reject(request.error);53 };54 });55}56 57// UPDATE - Modify existing record58async function updateTask(task) {59 return new Promise((resolve, reject) => {60 const transaction = db.transaction(['tasks'], 'readwrite');61 const store = transaction.objectStore('tasks');62 63 const request = store.put(task);64 65 request.onsuccess = () => {66 console.log('Task updated with key:', request.result);67 resolve(request.result);68 };69 70 request.onerror = () => {71 reject(request.error);72 };73 });74}75 76// DELETE - Remove record77async function deleteTask(id) {78 return new Promise((resolve, reject) => {79 const transaction = db.transaction(['tasks'], 'readwrite');80 const store = transaction.objectStore('tasks');81 82 const request = store.delete(id);83 84 request.onsuccess = () => {85 console.log('Task deleted');86 resolve(true);87 };88 89 request.onerror = () => {90 reject(request.error);91 };92 });93}Indexes and cursors are powerful features that enable efficient querying and iteration through large datasets in IndexedDB. Without indexes, retrieving records by properties other than the key requires scanning through all records in the object store, which becomes increasingly slow as the dataset grows. By creating indexes on frequently queried properties, you enable IndexedDB to quickly locate relevant records without full store scans. When creating indexes, the `unique` option ensures that no two records in the object store can have the same value for the indexed property. This constraint is enforced during record addition and modification operations, providing data integrity guarantees similar to unique constraints in relational databases. The `multiEntry` option is particularly powerful when dealing with array properties—when set to true, the index creates separate entries for each value in the array, enabling efficient queries that match any of the array values. Cursors provide a mechanism for iterating through records in an object store or index without loading all records into memory at once. Unlike `getAll()`, which returns all matching records simultaneously, cursors allow you to process records one at a time, making them ideal for handling large datasets or performing batch operations. The cursor maintains its position in the result set and advances only when you explicitly call `continue()` or one of the other navigation methods. Cursor ranges allow you to restrict which records the cursor iterates over using `IDBKeyRange`. This enables efficient iteration through subsets of data without loading unrelated records. The key range API supports four types of ranges: bound (a range between two keys), lowerBound (keys greater than or equal to a value), upperBound (keys less than or equal to a value), and only (a single key). These capabilities are essential for building [performant web applications](/services/web-development/) that handle large amounts of data efficiently.
1// Creating indexes during database setup2request.onupgradeneeded = (event) => {3 const database = event.target.result;4 const taskStore = database.createObjectStore('tasks', { keyPath: 'id' });5 6 // Index for status field (for filtering by status)7 taskStore.createIndex('status', 'status', { unique: false });8 9 // Index for due date (for sorting and range queries)10 taskStore.createIndex('dueDate', 'dueDate', { unique: false });11 12 // Multi-entry index for tags (array property)13 taskStore.createIndex('tags', 'tags', { unique: false, multiEntry: true });14};15 16// Using IDBKeyRange for queries17function createKeyRangeExamples() {18 // All keys less than a value19 const lowerBound = IDBKeyRange.lowerBound(100);20 21 // All keys greater than a value22 const upperBound = IDBKeyRange.upperBound(500);23 24 // Keys in a range (inclusive)25 const range = IDBKeyRange.bound(100, 500);26 27 // Exclusive bounds28 const exclusiveRange = IDBKeyRange.bound(100, 500, true, true);29 30 // Single key31 const singleKey = IDBKeyRange.only(123);32}33 34// Using cursors for iteration35async function getTasksByStatus(status) {36 return new Promise((resolve, reject) => {37 const transaction = db.transaction(['tasks'], 'readonly');38 const store = transaction.objectStore('tasks');39 const index = store.index('status');40 41 const request = index.openCursor(IDBKeyRange.only(status));42 const tasks = [];43 44 request.onsuccess = (event) => {45 const cursor = event.target.result;46 if (cursor) {47 tasks.push(cursor.value);48 cursor.continue(); // Move to next record49 } else {50 resolve(tasks);51 }52 };53 54 request.onerror = () => reject(request.error);55 });56}57 58// Advanced cursor with iteration direction59async function getTasksSortedByDueDate(direction = 'next') {60 return new Promise((resolve, reject) => {61 const transaction = db.transaction(['tasks'], 'readonly');62 const store = transaction.objectStore('tasks');63 const index = store.index('dueDate');64 65 const request = index.openCursor(null, direction);66 const tasks = [];67 68 request.onsuccess = (event) => {69 const cursor = event.target.result;70 if (cursor) {71 tasks.push(cursor.value);72 cursor.continue();73 } else {74 resolve(tasks);75 }76 };77 78 request.onerror = () => reject(request.error);79 });80}This section presents a complete, practical implementation of a TaskManager class that demonstrates production-ready patterns for IndexedDB usage. The implementation encapsulates all the concepts covered throughout this guide—database setup, object stores, transactions, CRUD operations, and indexes—into a clean, reusable API that you can adapt for your own projects. The TaskDatabase class demonstrates proper error handling, Promise-based APIs for clean async/await integration, connection management, and comprehensive CRUD operations including filtered queries. This represents the recommended approach for integrating IndexedDB into modern web applications, providing a solid foundation that can be extended with additional features like offline sync, data validation, and more complex querying capabilities as your application needs grow.
1class TaskDatabase {2 constructor() {3 this.dbName = 'TaskManagerDB';4 this.dbVersion = 1;5 this.db = null;6 }7 8 async open() {9 return new Promise((resolve, reject) => {10 const request = indexedDB.open(this.dbName, this.dbVersion);11 12 request.onsuccess = (event) => {13 this.db = event.target.result;14 resolve(this);15 };16 17 request.onerror = (event) => reject(event.target.error);18 19 request.onupgradeneeded = (event) => {20 const database = event.target.result;21 22 if (!database.objectStoreNames.contains('tasks')) {23 const taskStore = database.createObjectStore('tasks', {24 keyPath: 'id',25 autoIncrement: true26 });27 28 taskStore.createIndex('status', 'status', { unique: false });29 taskStore.createIndex('category', 'category', { unique: false });30 taskStore.createIndex('dueDate', 'dueDate', { unique: false });31 taskStore.createIndex('priority', 'priority', { unique: false });32 }33 };34 });35 }36 37 async addTask(task) {38 return this.performTransaction('tasks', 'readwrite', (store) => {39 const request = store.add({40 ...task,41 createdAt: new Date().toISOString(),42 updatedAt: new Date().toISOString()43 });44 return this.wrapRequest(request);45 });46 }47 48 async getTask(id) {49 return this.performTransaction('tasks', 'readonly', (store) => {50 return this.wrapRequest(store.get(id));51 });52 }53 54 async getAllTasks() {55 return this.performTransaction('tasks', 'readonly', (store) => {56 return this.wrapRequest(store.getAll());57 });58 }59 60 async updateTask(task) {61 return this.performTransaction('tasks', 'readwrite', (store) => {62 const updatedTask = {63 ...task,64 updatedAt: new Date().toISOString()65 };66 return this.wrapRequest(store.put(updatedTask));67 });68 }69 70 async deleteTask(id) {71 return this.performTransaction('tasks', 'readwrite', (store) => {72 return this.wrapRequest(store.delete(id));73 });74 }75 76 async getTasksByStatus(status) {77 return this.performTransaction('tasks', 'readonly', (store) => {78 const index = store.index('status');79 return this.wrapRequest(index.getAll(status));80 });81 }82 83 async getTasksByPriority(priority) {84 return this.performTransaction('tasks', 'readonly', (store) => {85 const index = store.index('priority');86 return this.wrapRequest(index.getAll(priority));87 });88 }89 90 async getOverdueTasks() {91 const now = new Date().toISOString();92 return this.performTransaction('tasks', 'readonly', (store) => {93 const index = store.index('dueDate');94 return this.wrapRequest(index.getAll(IDBKeyRange.upperBound(now)));95 });96 }97 98 async close() {99 if (this.db) {100 this.db.close();101 this.db = null;102 }103 }104 105 async performTransaction(storeName, mode, operation) {106 return new Promise((resolve, reject) => {107 const transaction = this.db.transaction(storeName, mode);108 109 transaction.oncomplete = () => resolve();110 transaction.onerror = () => reject(transaction.error);111 112 operation(transaction.objectStore(storeName))113 .then(resolve)114 .catch(reject);115 });116 }117 118 wrapRequest(request) {119 return new Promise((resolve, reject) => {120 request.onsuccess = () => resolve(request.result);121 request.onerror = () => reject(request.error);122 });123 }124}125 126// Usage127const db = new TaskDatabase();128 129async function example() {130 await db.open();131 132 // Add tasks133 await db.addTask({134 title: 'Complete project proposal',135 description: 'Write the initial project proposal document',136 status: 'pending',137 priority: 'high',138 category: 'work',139 dueDate: '2024-12-15T17:00:00Z'140 });141 142 // Query tasks143 const highPriorityTasks = await db.getTasksByPriority('high');144 const pendingTasks = await db.getTasksByStatus('pending');145 146 await db.close();147}Browsers impose storage quotas that limit how much data IndexedDB databases can hold. These quotas vary by browser, operating system, and available disk space. Modern browsers typically provide generous storage quotas for IndexedDB, often allowing applications to store hundreds of megabytes or even gigabytes of data depending on available disk space. When approaching quota limits, write operations fail with QuotaExceededError, so implementing cleanup strategies and monitoring storage usage helps prevent unexpected failures. The Storage Manager API provides tools for managing storage in modern browsers. The `navigator.storage.estimate()` method returns information about current storage usage and quota, allowing you to monitor how much space your application is using. The `navigator.storage.persist()` method requests persistent storage, which may prevent the browser from clearing your data under storage pressure. Understanding these APIs is essential for building applications that gracefully handle storage constraints and provide appropriate feedback to users. Some browsers provide different storage behaviors in private or incognito modes, where IndexedDB may be treated as ephemeral and data is cleared when the browser session ends. Additionally, browser vendors have implemented different policies for handling storage when disk space becomes limited. Implementing proper error handling for QuotaExceededError is critical—your application should catch this error and implement strategies such as cleaning up old data, compressing stored data, or alerting the user that storage is full. Database connections should be managed carefully to avoid memory leaks and resource exhaustion. Each call to `indexedDB.open()` creates a new connection, and failing to close unused connections can lead to performance issues, especially in long-running applications. The best practice is to open connections when needed, perform operations, and close connections promptly. This connection management is a key aspect of [application performance optimization](/services/web-development/) when working with IndexedDB.
1// Check storage estimate using StorageManager API2async function getStorageEstimate() {3 if (navigator.storage && navigator.storage.estimate) {4 const estimate = await navigator.storage.estimate();5 console.log('Usage:', estimate.usage);6 console.log('Quota:', estimate.quota);7 console.log('Percent used:', ((estimate.usage / estimate.quota) * 100).toFixed(2) + '%');8 9 return {10 usage: estimate.usage,11 quota: estimate.quota,12 percentUsed: (estimate.usage / estimate.quota) * 10013 };14 }15 return null;16}17 18// Request persistent storage19async function requestPersistentStorage() {20 if (navigator.storage && navigator.storage.persist) {21 const isPersistent = await navigator.storage.persist();22 console.log('Persistent storage granted:', isPersistent);23 return isPersistent;24 }25 return false;26}27 28// Handle quota exceeded errors29function handleQuotaExceeded(operation) {30 try {31 return operation();32 } catch (error) {33 if (error.name === 'QuotaExceededError') {34 console.error('Storage quota exceeded!');35 // Strategies:36 // 1. Clean up old data37 // 2. Compress stored data38 // 3. Notify user to free up space39 // 4. Fall back to smaller storage solution40 throw new Error('Unable to store data: quota exceeded. Please clear some data and try again.');41 }42 throw error;43 }44}Progressive Web Apps commonly use IndexedDB to cache application data and assets for offline functionality, enabling users to continue working even when network connectivity is unavailable or unreliable. The offline-first architecture pattern stores data locally first, then syncs with backend APIs when connectivity returns. This approach ensures that users can always make progress on their tasks, even in challenging network conditions, and their changes are preserved once they reconnect. Implementing offline sync requires a queue pattern where operations performed while offline are stored in IndexedDB and processed when the application detects network connectivity. The SyncQueue pattern demonstrated here stores pending operations with metadata about when they were created and how many times they've been attempted. When the application comes back online, it processes the queue, sending each operation to the backend and removing successfully synced items. Service Workers complement IndexedDB by handling network request interception and caching. Together, they create a powerful combination for building PWAs that work reliably offline. The service worker can cache application shell assets and API responses, while IndexedDB stores structured data that needs to be persisted and synced. This combination is fundamental to modern [Progressive Web App development](/services/web-development/) and provides a native-app-like experience in the browser. Conflict resolution is an important consideration when syncing offline changes with a backend. When the same data is modified both locally and on the server, you need strategies to determine which version takes precedence. Common approaches include last-write-wins, server-wins, merge strategies, and user-initiated conflict resolution. The appropriate strategy depends on your application's data model and user expectations. For projects using Node.js backends, you might also explore our guide on [file uploads with Multer in Node.js and Express](/resources/guides/web-design/multer-nodejs-express-upload-file/) to handle synced file data.
1// SyncQueue - Manage offline operations for PWA sync2class SyncQueue {3 constructor(db) {4 this.db = db;5 }6 7 async init() {8 // Create sync queue store during setup9 return new Promise((resolve, reject) => {10 const transaction = this.db.transaction(['syncQueue'], 'readwrite');11 const store = transaction.objectStore('syncQueue');12 13 transaction.oncomplete = () => resolve();14 transaction.onerror = () => reject(transaction.error);15 16 if (!this.db.objectStoreNames.contains('syncQueue')) {17 this.db.createObjectStore('syncQueue', {18 keyPath: 'id',19 autoIncrement: true20 });21 }22 });23 }24 25 async queueOperation(operation) {26 // Store operation for later sync27 await this.addToQueue({28 operation: operation.type,29 data: operation.data,30 timestamp: Date.now(),31 retryCount: 032 });33 }34 35 async addToQueue(item) {36 return new Promise((resolve, reject) => {37 const transaction = this.db.transaction(['syncQueue'], 'readwrite');38 const store = transaction.objectStore('syncQueue');39 40 const request = store.add(item);41 42 request.onsuccess = () => resolve(request.result);43 request.onerror = () => reject(request.error);44 });45 }46 47 async processQueue() {48 if (!navigator.onLine) return false;49 50 const items = await this.getQueueItems();51 52 for (const item of items) {53 try {54 await this.syncItem(item);55 await this.removeFromQueue(item.id);56 } catch (error) {57 console.error('Sync failed for item:', item.id, error);58 await this.incrementRetryCount(item.id);59 }60 }61 62 return true;63 }64 65 async getQueueItems() {66 return new Promise((resolve, reject) => {67 const transaction = this.db.transaction(['syncQueue'], 'readonly');68 const store = transaction.objectStore('syncQueue');69 const request = store.getAll();70 71 request.onsuccess = () => resolve(request.result);72 request.onerror = () => reject(request.error);73 });74 }75 76 async syncItem(item) {77 // Implement actual API calls here78 console.log('Syncing item:', item);79 // Example:80 // await fetch('/api/sync', {81 // method: 'POST',82 // body: JSON.stringify(item)83 // });84 }85 86 async removeFromQueue(id) {87 return new Promise((resolve, reject) => {88 const transaction = this.db.transaction(['syncQueue'], 'readwrite');89 const store = transaction.objectStore('syncQueue');90 const request = store.delete(id);91 92 request.onsuccess = () => resolve();93 request.onerror = () => reject(request.error);94 });95 }96 97 async incrementRetryCount(id) {98 const item = await this.getItem(id);99 item.retryCount++;100 return this.updateQueueItem(item);101 }102 103 async getItem(id) {104 return new Promise((resolve, reject) => {105 const transaction = this.db.transaction(['syncQueue'], 'readonly');106 const store = transaction.objectStore('syncQueue');107 const request = store.get(id);108 109 request.onsuccess = () => resolve(request.result);110 request.onerror = () => reject(request.error);111 });112 }113 114 async updateQueueItem(item) {115 return new Promise((resolve, reject) => {116 const transaction = this.db.transaction(['syncQueue'], 'readwrite');117 const store = transaction.objectStore('syncQueue');118 const request = store.put(item);119 120 request.onsuccess = () => resolve();121 request.onerror = () => reject(request.error);122 });123 }124}125 126// Listen for online events to trigger sync127window.addEventListener('online', async () => {128 console.log('Back online - processing sync queue');129 const syncQueue = new SyncQueue(db);130 await syncQueue.processQueue();131});Working effectively with IndexedDB requires understanding common pitfalls and following established best practices. These guidelines will help you build robust, performant applications that handle data reliably and scale well as your data needs grow. Following these patterns from the start will save significant refactoring effort as your application evolves. Key areas to focus on include proper transaction management, error handling at the appropriate levels, efficient bulk operations, and careful connection management. Understanding the asynchronous nature of IndexedDB and structuring your code accordingly is essential for building applications that perform well and maintain data integrity under load. For testing IndexedDB operations in your application, consider using [Puppeteer for automated UI testing](/resources/guides/web-design/using-puppeteer-for-automated-ui-testing/) to ensure your storage layer works correctly across browsers. These practices become especially important when building [enterprise-grade web applications](/services/web-development/) that must handle significant amounts of data.
IndexedDB represents a powerful solution for client-side storage needs that go beyond what localStorage can offer. Its support for large data volumes, complex querying through indexes, ACID-compliant transactions, and offline-first capabilities make it an essential technology for building modern web applications that rival native app experiences. Whether you're building Progressive Web Apps, productivity tools, or data-intensive dashboards, IndexedDB provides the foundation for reliable local data management. Throughout this guide, we've covered the essential concepts from basic database setup and object stores through advanced patterns like offline sync queues. The key to success with IndexedDB lies in understanding its asynchronous, transactional nature and structuring your code accordingly. By following the best practices outlined here—proper error handling, efficient connection management, and thoughtful indexing strategies—you can build applications that perform well and maintain data integrity even under challenging conditions. As you implement IndexedDB in your own projects, remember that the patterns demonstrated here are starting points that can be adapted and extended for your specific needs. The combination of IndexedDB for data storage, Service Workers for network handling, and modern JavaScript patterns for state management creates a powerful foundation for building exceptional user experiences. If you're looking to implement these patterns in a production application, our [web development team](/services/web-development/) has extensive experience building offline-first applications and can help you deliver a seamless experience to your users.
Our expert team specializes in Progressive Web Apps, offline-first architecture, and modern web development solutions.