Skip to main content

IndexedDB

Overview

IndexedDB is a low-level API for client-side storage of significant amounts of structured data, including files and blobs.

  • Large-scale NoSQL database in the browser
  • Data is stored as key-value pairs in object stores
  • Supports transactions (mandatory for all read/write operations)
  • Accessible in window, service workers, and web workers

When to use IndexedDB

In PWA (Progressive Web App) architecture:

  • URL-addressable resources (HTML, CSS, JS, images) — use the Cache API
  • Everything else (application data, user state) — use IndexedDB

For simple key-value data, localStorage is simpler. Use IndexedDB when you need structured data, large storage, or offline support.

Terminology

  • Database — contains object stores, which contain data. Typically one DB per app.
  • Object store — similar to tables in a relational DB. Data is stored as JS objects (no strict schema).
  • Index — used to organize data in an object store by a specific property (enables efficient lookups).
  • Transaction — same concept as in SQL. Mandatory in IndexedDB for all read/write operations (ensures thread safety).
  • Cursor — mechanism to iterate over multiple records in a DB.

idb library

The raw IndexedDB API is not very developer-friendly. Use a wrapper library.

idb — Jake Archibald's IndexedDB Promised library. Thin, promise-based wrapper written in TypeScript. https://github.com/jakearchibald/idb

Define DB schema

Every property in the schema is an object store:

  • value — the type stored
  • key — the key type
  • indexes — index name and data value type
interface IListDB extends DBSchema {
items: {
value: {
id: string;
description: string;
completed: boolean;
};
key: string;
indexes: { 'by-completed': boolean };
};
}

Open and migrate DB

Open the database and run upgrade logic when the version changes:

const db = await openDB<IListDB>('list-db', VERSION, {
upgrade(db) {
if (db.objectStoreNames.contains('items')) {
db.deleteObjectStore('items');
}
const itemsStore = db.createObjectStore('items', {
keyPath: 'id',
});
itemsStore.createIndex('by-completed', 'completed');
console.log("DB upgraded!");
},
});

Save data

data.forEach(async item => {
await db.put('items', item);
});

Read/write with transactions

Normally read/write operations are wrapped in a transaction:

const tx = db.transaction('items', 'readwrite');
const store = tx.objectStore('items');
const val = (await store.get('some guid'));
await store.put({...val!, completed: true});
await tx.done;

NB! Do not await other things inside a transaction — the transaction auto-closes when there are no pending requests, so awaiting unrelated promises will cause it to fail.

Single commands (shortcuts)

It's common to use single commands instead of explicit transactions, so shortcuts are provided by the idb library:

  • get, getKey, getAll, getAllKeys, count, put, add, delete, clear
  • getFromIndex, getKeyFromIndex, getAllFromIndex, getAllKeysFromIndex, countFromIndex