IndexedDB for Offline Data — Structured Storage in the Browser
In this tutorial, you will learn about IndexedDB for Offline Data. We cover key concepts, practical examples, and best practices to help you master this topic.
IndexedDB stores structured data offline in the browser with indexes, transactions, and large storage limits, enabling complex offline functionality beyond simple Caching of network responses.
What You'll Learn
By the end of this tutorial, you will understand how IndexedDB works, how to create databases and object stores, how to perform CRUD operations, and how to use it alongside Cache Storage for complete offline support.
Why It Matters
Cache Storage is great for HTTP responses but cannot query or filter data. IndexedDB fills this gap by providing a full NoSQL database in the browser. For offline-first apps that need search, filtering, or complex data operations, IndexedDB is essential.
Real-World Use
A note-taking PWA stores all notes in IndexedDB. Users create, edit, search, and delete notes entirely offline. When connectivity returns, a background sync pushes changes to the server. The Cache API caches the app shell and UI assets, while IndexedDB stores the actual user data.
IndexedDB vs Cache Storage
IndexedDB vs Cache API
┌──────────────────────────────────────────────────────────┐
│ Which Storage to Use? │
├──────────────────────────────┬───────────────────────────┤
│ Cache API │ IndexedDB │
├──────────────────────────────┼───────────────────────────┤
│ HTTP Request/Response pairs │ Any structured data │
│ Keyed by URL │ Keyed by custom keys │
│ No querying │ Indexed for querying │
│ No transactions │ ACID transactions │
│ Best for assets │ Best for app data │
│ ~50MB typical limit │ ~GB+ storage limit │
└──────────────────────────────┴───────────────────────────┘
Think of Cache Storage like a filing cabinet organized by URL — you look up a file by address. IndexedDB is like a database — you can search, filter, sort, and run complex queries on your data.
Opening a Database
// Open (or create) an IndexedDB database
const request = indexedDB.open('MyAppDB', 1);
request.onerror = (event) => {
console.error('Database error:', event.target.error);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
console.log('Creating/upgrading database');
// Create an object store (like a table)
const store = db.createObjectStore('notes', {
keyPath: 'id',
autoIncrement: true
});
// Create indexes for querying
store.createIndex('title', 'title', { unique: false });
store.createIndex('createdAt', 'createdAt', { unique: false });
store.createIndex('tags', 'tags', { unique: false, multiEntry: true });
console.log('Database schema created');
};
request.onsuccess = (event) => {
const db = event.target.result;
console.log('Database opened successfully');
console.log('DB name:', db.name);
console.log('DB version:', db.version);
};
Output:
Creating/upgrading database
Database schema created
Database opened successfully
DB name: MyAppDB
DB version: 1
CRUD Operations
Create
function addNote(db, note) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['notes'], 'readwrite');
const store = transaction.objectStore('notes');
const request = store.add({
title: note.title,
content: note.content,
tags: note.tags || [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
synced: false
});
request.onsuccess = () => {
console.log('Note added with id:', request.result);
resolve(request.result);
};
request.onerror = () => {
console.error('Error adding note:', request.error);
reject(request.error);
};
});
}
// Usage
const dbRequest = indexedDB.open('MyAppDB', 1);
dbRequest.onsuccess = (event) => {
const db = event.target.result;
addNote(db, {
title: 'My First Offline Note',
content: 'This note was created while offline!',
tags: ['pwa', 'offline']
});
};
Output:
Note added with id: 1
Read
function getNote(db, id) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['notes'], 'readonly');
const store = transaction.objectStore('notes');
const request = store.get(id);
request.onsuccess = () => {
if (request.result) {
console.log('Found note:', request.result.title);
} else {
console.log('Note not found');
}
resolve(request.result);
};
request.onerror = () => reject(request.error);
});
}
function getAllNotes(db) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['notes'], 'readonly');
const store = transaction.objectStore('notes');
const request = store.getAll();
request.onsuccess = () => {
console.log(`Found ${request.result.length} notes`);
resolve(request.result);
};
request.onerror = () => reject(request.error);
});
}
Output:
Found note: My First Offline Note
Found 5 notes
Update
function updateNote(db, id, updates) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['notes'], 'readwrite');
const store = transaction.objectStore('notes');
// First get the existing note
const getRequest = store.get(id);
getRequest.onsuccess = () => {
const note = getRequest.result;
if (!note) {
reject(new Error('Note not found'));
return;
}
// Merge updates
Object.assign(note, updates, {
updatedAt: new Date().toISOString(),
synced: false
});
const putRequest = store.put(note);
putRequest.onsuccess = () => {
console.log('Note updated:', id);
resolve(note);
};
putRequest.onerror = () => reject(putRequest.error);
};
getRequest.onerror = () => reject(getRequest.error);
});
}
Delete
function deleteNote(db, id) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['notes'], 'readwrite');
const store = transaction.objectStore('notes');
const request = store.delete(id);
request.onsuccess = () => {
console.log('Note deleted:', id);
resolve();
};
request.onerror = () => reject(request.error);
});
}
Querying with Indexes
function findNotesByTag(db, tag) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['notes'], 'readonly');
const store = transaction.objectStore('notes');
const tagIndex = store.index('tags');
// Use the multiEntry index to find all notes with the tag
const request = tagIndex.getAll(tag);
request.onsuccess = () => {
console.log(`Found ${request.result.length} notes tagged "${tag}"`);
resolve(request.result);
};
request.onerror = () => reject(request.error);
});
}
function findNotesByDateRange(db, startDate, endDate) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['notes'], 'readonly');
const store = transaction.objectStore('notes');
const dateIndex = store.index('createdAt');
// Range query
const range = IDBKeyRange.bound(startDate, endDate);
const request = dateIndex.getAll(range);
request.onsuccess = () => {
console.log(`Found ${request.result.length} notes in date range`);
resolve(request.result);
};
request.onerror = () => reject(request.error);
});
}
IndexedDB in Service Workers
// IndexedDB is available inside service workers
self.addEventListener('message', event => {
if (event.data.type === 'GET_OFFLINE_NOTES') {
const dbRequest = indexedDB.open('MyAppDB', 1);
dbRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction(['notes'], 'readonly');
const store = transaction.objectStore('notes');
const getAll = store.getAll();
getAll.onsuccess = () => {
event.ports[0].postMessage({
type: 'OFFLINE_NOTES',
notes: getAll.result
});
};
};
}
});
Common Mistakes
- Not handling database version upgrades. When you change the schema, increment the version and handle onupgradeneeded. Failing to do this causes errors for returning users.
- Using synchronous patterns. IndexedDB is always asynchronous. Use Promises or async/await, not synchronous code patterns.
- Creating too many indexes. Each index adds write overhead. Index only fields you actually query.
- Not closing database connections. Keep connections open for the page lifetime but close them when the page unloads or in service workers after operations complete.
- Storing large binary blobs in the main thread. Store large blobs (images, files) in IndexedDB but access them in a Web Worker to avoid blocking the UI.
Practice Questions
- What types of data are better suited for IndexedDB versus the Cache API?
- How do you create indexes in IndexedDB and why are they useful?
- What is a Transaction in IndexedDB and why is it important for data integrity?
- How does versioning work in IndexedDB schema migrations?
- How can IndexedDB be used in a service worker?
Challenge: Build an IndexedDB-based offline notes app with: create, read, update, delete operations, a tag-based index for filtering, and a date-range query. Persist notes across page reloads and verify data survives going offline.
FAQ
Mini Project
Create an offline-capable note-taking app that: stores notes in IndexedDB with title, content, tags, and timestamps; supports CRUD operations; indexes by tags and creation date; syncs with a mock server when online; and uses Cache Storage for the app shell. Test by creating notes offline, then going online to sync.
What's Next
You can store structured data offline. Now learn about push notifications to re-engage users even when the browser is closed.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro