IndexedDB Object Stores and Indexes in JavaScript

IndexedDB is a powerful, low-level API for client-side storage that allows web applications to store significant amounts of structured data, including files and blobs. This article covers how IndexedDB organizes data using object stores—the equivalent of tables in relational databases—and how it utilizes indexes to enable high-performance querying and sorting of stored JavaScript objects.

The IndexedDB Architecture Hierarchy

At its core, IndexedDB follows a distinct hierarchical structure:

  1. Database: The highest level of organization. An application can have multiple databases, identified by name and version.
  2. Object Stores: The primary storage mechanism within a database, holding individual data records.
  3. Records: Individual JavaScript objects or primitive values stored within an object store.
  4. Indexes: Specialized structures tied to an object store that enable querying records by specific object properties.

Object Stores: The Foundation of Storage

An object store holds records as key-value pairs, where the value is typically a JavaScript object. Unlike relational database tables, object stores are schema-less; records within the same store do not need to share the same structure.

Key Paths and Key Generators

Every record in an object store must have a unique primary key. IndexedDB handles keys in two main ways:

// Creating an object store in the onupgradeneeded event
const request = indexedDB.open('AppDatabase', 1);

request.onupgradeneeded = (event) => {
  const db = event.target.result;

  // Create an object store with an in-line auto-incrementing key
  const userStore = db.createObjectStore('users', {
    keyPath: 'id',
    autoIncrement: true,
  });
};

Indexes: Enabling Efficient Lookups

By default, an object store only allows direct retrieval of records using the primary key. To query or filter data based on other properties within stored objects, you must create indexes.

An index is a specialized lookup table created on a specific property (or array of properties) of the objects held in an object store.

Creating an Index

Indexes must be created within the onupgradeneeded lifecycle event using the createIndex method:

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  const userStore = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });

  // Create an index for unique email lookups
  userStore.createIndex('email_idx', 'email', { unique: true });

  // Create an index for querying by role (non-unique)
  userStore.createIndex('role_idx', 'role', { unique: false });
};

Index Configuration Options

Querying Data via Object Stores vs. Indexes

Interacting with data requires opening a transaction on the target object store:

1. Direct Lookup by Primary Key

const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const getRequest = store.get(1); // Retrieves user with id = 1

getRequest.onsuccess = () => {
  console.log(getRequest.result);
};

2. Lookup Using an Index

const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const emailIndex = store.index('email_idx');

const queryRequest = emailIndex.get('alex@example.com');

queryRequest.onsuccess = () => {
  console.log(queryRequest.result);
};

3. Range Queries and Cursors

For retrieving multiple records matching a condition, indexes can be paired with IDBKeyRange and cursors:

const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const roleIndex = store.index('role_idx');

// Query all users where role is "admin"
const range = IDBKeyRange.only('admin');
const cursorRequest = roleIndex.openCursor(range);

cursorRequest.onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    console.log(cursor.value);
    cursor.continue(); // Move to next record
  }
};

Summary

IndexedDB structures data by decoupling primary storage (object stores) from query access patterns (indexes). Object stores preserve structured JavaScript data via primary keys, while indexes project secondary keys onto that data, allowing fast, filtered, and sorted retrieval directly on the client side.