This guide covers the basics of the
[IndexedDB API](https://developer.mozilla.org/docs/Web/API/IndexedDB_API).
We're using Jake Archibald's
[IndexedDB Promised](https://github.com/jakearchibald/indexeddb-promised)
library, which is very similar to the IndexedDB API, but uses promises, which
you can `await` for more concise syntax. This simplifies the API while
maintaining its structure.

## What is IndexedDB?

IndexedDB is a large-scale, NoSQL storage system that allows storage of just
about anything in the user's browser. In addition to the usual search, get, and
put actions, IndexedDB also supports transactions, and it's well suited for
storing large amounts of structured data.

Each IndexedDB database is unique to an [origin](https://developer.mozilla.org/docs/Glossary/Origin)
(typically the site domain or subdomain), meaning it can't access or be accessed
by any other origin. Its [data storage limits](https://web.dev/articles/storage-for-the-web)
are usually large, if they exist at all, but different browsers handle limits
and data eviction differently. See the [Further reading](https://web.dev/articles/indexeddb#resources) section for
more information.

## IndexedDB terms

[Database](https://developer.mozilla.org/docs/Web/API/IDBDatabase)
:   The highest level of IndexedDB. It contains the object stores, which in turn
    contain the data you want to persist. You can create multiple databases with
    whatever names you choose.

[Object store](https://developer.mozilla.org/docs/Web/API/IDBObjectStore)
:   An individual bucket to store data, similar to tables in relational databases.
    Typically, there is one object store for each *type* (not JavaScript data
    type) of data you're storing. Unlike in database tables, the JavaScript data
    types of data in a store don't need to be consistent. For example, if an app
    has a `people` object store containing information about three people, those
    people's age properties could be `53`, `'twenty-five'`, and `unknown`.

[Index](https://developer.mozilla.org/docs/Web/API/IDBIndex)
:   A kind of object store for organizing data in another object store (called the
    reference object store) by an individual property of the data. The index is used
    to retrieve records in the object store by this property. For example, if you're
    storing people, you might want to fetch them later by their name, age, or
    favorite animal.

Operation
:   An interaction with the database.

[Transaction](https://developer.mozilla.org/docs/Web/API/IDBTransaction)
:   A wrapper around an operation or group of operations that ensures database
    integrity. If one of the actions in a transaction fails, none of them are
    applied and the database returns to the state it was in before the transaction
    began. All read or write operations in IndexedDB must be part of a transaction.
    This allows atomic read-modify-write operations without the risk of conflicts
    with other threads acting on the database at the same time.

[Cursor](https://developer.mozilla.org/docs/Web/API/IDBCursor)
:   A mechanism for iterating over multiple records in a database.

## How to check for IndexedDB support

IndexedDB is almost [universally supported](https://caniuse.com/indexeddb).
However, if you're working with older browsers, it's not a bad idea to
feature-detect support just in case. The easiest way is to check the `window`
object:

    function indexedDBStuff () {
      // Check for IndexedDB support:
      if (!('indexedDB' in window)) {
        // Can't use IndexedDB
        console.log("This browser doesn't support IndexedDB");
        return;
      } else {
        // Do IndexedDB stuff here:
        // ...
      }
    }

    // Run IndexedDB code:
    indexedDBStuff();

> [!NOTE]
> **Note:** Because the majority of browsers out there support IndexedDB, the examples in this guide omit the feature-checking code for the sake of brevity. If a significant portion of your users are on legacy browsers, you should include feature checking in your IndexedDB workflow.

## How to open a database

With IndexedDB, you can create multiple databases with any names you choose. If
a database doesn't exist when you try to open it, it's' automatically created.
To open a database, use the `openDB()` method from the `idb` library:

    import {openDB} from 'idb';

    async function useDB () {
      // Returns a promise, which makes `idb` usable with async-await.
      const dbPromise = await openDB('example-database', version, events);
    }

    useDB();

This method returns a promise that resolves to a database object. When using the
`openDB()` method, provide a name, version number, and an events object to set
up the database.

Here's an example of the `openDB()` method in context:

    import {openDB} from 'idb';

    async function useDB () {
      // Opens the first version of the 'test-db1' database.
      // If the database does not exist, it will be created.
      const dbPromise = await openDB('test-db1', 1);
    }

    useDB();

Place the check for IndexedDB support at the top of the anonymous function. This
exits the function if the browser doesn't support IndexedDB. If the function can
continue, it calls the `openDB()` method to open a database named `'test-db1'`.
In this example, the optional events object has been left out to keep things
simple, but you need to specify it to do any meaningful work with IndexedDB.

## How to work with object stores

An IndexedDB database contains one or more *object stores*, which each have a
column for a key, and another column for the data associated with that key.

### Create object stores

A well structured IndexedDB database should have one object store for each type
of data that needs to be persisted. For example, a site that persists user
profiles and notes might have a `people` object store containing `person`
objects, and a `notes` object store containing `note` objects.

To ensure database integrity, you can only create or remove object stores in the
events object in an `openDB()` call. The events object exposes a `upgrade()`
method that lets you create object stores. Call the
[`createObjectStore()`](https://developer.mozilla.org/docs/Web/API/IDBDatabase/createObjectStore)
method inside the `upgrade()` method to create the object store:

    import {openDB} from 'idb';

    async function createStoreInDB () {
      const dbPromise = await openDB('example-database', 1, {
        upgrade (db) {
          // Creates an object store:
          db.createObjectStore('storeName', options);
        }
      });
    }

    createStoreInDB();

This method takes the name of the object store and an optional configuration
object that lets you define various properties for the object store.

The following is an example of how to use `createObjectStore()`:

    import {openDB} from 'idb';

    async function createStoreInDB () {
      const dbPromise = await openDB('test-db1', 1, {
        upgrade (db) {
          console.log('Creating a new object store...');

          // Checks if the object store exists:
          if (!db.objectStoreNames.contains('people')) {
            // If the object store does not exist, create it:
            db.createObjectStore('people');
          }
        }
      });
    }

    createStoreInDB();

In this example, an events object is passed to the `openDB()` method to create
the object store, and as before, the work of creating the object store is done
in the event object's `upgrade()` method. However, because the browser throws an
error if you try to create an object store that already exists, we recommend
wrapping the `createObjectStore()` method in an `if` statement that checks
whether the object store exists. Inside the `if` block, call
`createObjectStore()` to create an object store named `'firstOS'`.

### How to define primary keys

When you define object stores, you can define how data is uniquely identified in
the store using a primary key. You can define a primary key either by defining a
key path or by using a key generator.

A *key path* is a property that always exists and contains a unique value. For
example, in the case of a `people` object store, you might choose the email
address as the key path:

    import {openDB} from 'idb';

    async function createStoreInDB () {
      const dbPromise = await openDB('test-db2', 1, {
        upgrade (db) {
          if (!db.objectStoreNames.contains('people')) {
            db.createObjectStore('people', { keyPath: 'email' });
          }
        }
      });
    }

    createStoreInDB();

This example creates an object store called `'people'` and assigns the `email`
property as the primary key in the `keyPath` option.

You can also use a key generator such as `autoIncrement`. The key generator
creates a unique value for every object added to the object store. By default,
if you don't specify a key, IndexedDB creates a key and stores it separately
from the data.

The following example creates an object store called `'notes'` and sets the
primary key to be assigned automatically as an auto-incrementing number:

    import {openDB} from 'idb';

    async function createStoreInDB () {
      const dbPromise = await openDB('test-db2', 1, {
        upgrade (db) {
          if (!db.objectStoreNames.contains('notes')) {
            db.createObjectStore('notes', { autoIncrement: true });
          }
        }
      });
    }

    createStoreInDB();

The following example is similar to the previous example, but this time the
auto-incrementing value is explicitly assigned to a property named `'id'`.

    import {openDB} from 'idb';

    async function createStoreInDB () {
      const dbPromise = await openDB('test-db2', 1, {
        upgrade (db) {
          if (!db.objectStoreNames.contains('logs')) {
            db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
          }
        }
      });
    }

    createStoreInDB();

Choosing which method to use to define the key depends on your data. If your
data has a property that is always unique, you can make it the `keyPath` to
enforce this uniqueness. Otherwise, use an auto-incrementing value.

The following code creates three object stores demonstrating the various ways of
defining primary keys in object stores:

    import {openDB} from 'idb';

    async function createStoresInDB () {
      const dbPromise = await openDB('test-db2', 1, {
        upgrade (db) {
          if (!db.objectStoreNames.contains('people')) {
            db.createObjectStore('people', { keyPath: 'email' });
          }

          if (!db.objectStoreNames.contains('notes')) {
            db.createObjectStore('notes', { autoIncrement: true });
          }

          if (!db.objectStoreNames.contains('logs')) {
            db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
          }
        }
      });
    }

    createStoresInDB();

### How to define indexes

Indexes are a kind of object store used to retrieve data from the reference
object store by a specified property. An index lives inside the reference object
store and contains the same data, but uses the specified property as its
key path instead of the reference store's primary key. Indexes must be made when
you create your object stores, and can be used to define a unique constraint on
your data.

To create an index, call the [`createIndex()`](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/createIndex)
method on an object store instance:

    import {openDB} from 'idb';

    async function createIndexInStore() {
      const dbPromise = await openDB('storeName', 1, {
        upgrade (db) {
          const objectStore = db.createObjectStore('storeName');

          objectStore.createIndex('indexName', 'property', options);
        }
      });
    }

    createIndexInStore();

This method creates and returns an index object. The `createIndex()` method on
the object store's instance takes the name of the new index as the first
argument, and the second argument refers to the property on the data you want to
index. The final argument lets you define two options that determine how the
index operates: `unique` and `multiEntry`. If `unique` is set to `true`, the
index doesn't allow duplicate values for a single key. Next, `multiEntry`
determines how `createIndex()` behaves when the indexed property is an array. If
it's set to `true`, `createIndex()` adds an entry in the index for each array
element. Otherwise, it adds a single entry containing the array.

Here's an example:

    import {openDB} from 'idb';

    async function createIndexesInStores () {
      const dbPromise = await openDB('test-db3', 1, {
        upgrade (db) {
          if (!db.objectStoreNames.contains('people')) {
            const peopleObjectStore = db.createObjectStore('people', { keyPath: 'email' });

            peopleObjectStore.createIndex('gender', 'gender', { unique: false });
            peopleObjectStore.createIndex('ssn', 'ssn', { unique: true });
          }

          if (!db.objectStoreNames.contains('notes')) {
            const notesObjectStore = db.createObjectStore('notes', { autoIncrement: true });

            notesObjectStore.createIndex('title', 'title', { unique: false });
          }

          if (!db.objectStoreNames.contains('logs')) {
            const logsObjectStore = db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
          }
        }
      });
    }

    createIndexesInStores();

In this example, the `'people'` and `'notes'` object stores have indexes. To
create the indexes, first assign the result of `createObjectStore()` (an object
store object) to a variable so you can call `createIndex()` on it.

> [!NOTE]
> **Note:** Indexes are updated every time you write data to the reference object store. More indexes means more work for IndexedDB.

## How to work with data

This section describes how to create, read, update, and delete data. These
operations are all asynchronous, using promises where the IndexedDB API uses
requests. This simplifies the API. Instead of listening for events triggered by
the request, you can call `.then()` on the database object returned from the
`openDB()` method to start interactions with the database, or `await` its
creation.

All data operations in IndexedDB are carried out inside a transaction. Each
operation has the following form:

1. Get database object.
2. Open transaction on database.
3. Open object store on transaction.
4. Perform operation on object store.

A transaction can be thought of as a safe wrapper around an operation or group
of operations. If one of the actions within a transaction fails, all of the
actions are rolled back. Transactions are specific to one or more object stores,
which you define when you open the transaction. They can be read-only or read
and write. This signifies whether the operations inside the transaction read the
data or make a change to the database.

### Create data

To create data, call the [`add()`](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/add)
method on the database instance and pass in the data you want to add. The `add()`
method's first argument is the object store you want to add the data to, and the
second argument is an object containing the fields and associated data you want
to add. Here's the simplest example, where a single row of data is added:

    import {openDB} from 'idb';

    async function addItemToStore () {
      const db = await openDB('example-database', 1);

      await db.add('storeName', {
        field: 'data'
      });
    }

    addItemToStore();

> [!NOTE]
> **Note:** If the object store has a defined `keyPath`, the data must contain this property and the value must be unique. The `add()` method returns a promise that resolves after the object is added to the store.

Each `add()` call happens within a transaction, so even if the promise resolves
successfully, it doesn't necessarily mean the operation worked. To make sure
the add operation was carried out, you need to check whether the whole
transaction has completed using the `transaction.done()` method. This is a
promise that resolves when the transaction completes itself, and rejects if the
transaction errors. You must perform this check for all "write" operations,
because it's your only way of knowing the changes to the database have actually
happened.

The following code shows use of the `add()` method inside a transaction:

    import {openDB} from 'idb';

    async function addItemsToStore () {
      const db = await openDB('test-db4', 1, {
        upgrade (db) {
          if (!db.objectStoreNames.contains('foods')) {
            db.createObjectStore('foods', { keyPath: 'name' });
          }
        }
      });
      
      // Create a transaction on the 'foods' store in read/write mode:
      const tx = db.transaction('foods', 'readwrite');

      // Add multiple items to the 'foods' store in a single transaction:
      await Promise.all([
        tx.store.add({
          name: 'Sandwich',
          price: 4.99,
          description: 'A very tasty sandwich!',
          created: new Date().getTime(),
        }),
        tx.store.add({
          name: 'Eggs',
          price: 2.99,
          description: 'Some nice eggs you can cook up!',
          created: new Date().getTime(),
        }),
        tx.done
      ]);
    }

    addItemsToStore();

Once you open the database (and create an object store if needed), you'll need
to open a transaction by calling the `transaction()` method on it. This method
takes an argument for the store you want to transact on, as well as the mode.
In this case, we're interested in writing to the store, so this example
specifies `'readwrite'`.

The next step is to begin adding items to the store as part of the transaction.
In the preceding example, we're dealing with three operations on the `'foods'`
store that each return a promise:

1. Adding a record for a tasty sandwich.
2. Adding a record for some eggs.
3. Signalling that the transaction is complete (`tx.done`).

Because all of these actions are all promise-based, we need to wait for all of
them to finish. Passing these promises to
[`Promise.all`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise/all)
is a nice, ergonomic way to get this done. `Promise.all` accepts an array of
promises and finishes when all the promises passed to it have resolved.

For the two records being added, the transaction instance's `store` interface
calls `add()` and passes the data to it. You can `await` the `Promise.all` call
so it finishes when the transaction completes.

### Read data

To read data, call the [`get()`](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/get)
method on the database instance you retrieve using the `openDB()` method.
`get()` takes the name of the store and the primary key value of the object you
want to retrieve. Here's a basic example:

    import {openDB} from 'idb';

    async function getItemFromStore () {
      const db = await openDB('example-database', 1);

      // Get a value from the object store by its primary key value:
      const value = await db.get('storeName', 'unique-primary-key-value');
    }

    getItemFromStore();

As with `add()`, the `get()` method returns a promise, so you can `await` it if
you prefer, or use the promise's `.then()` callback.

The following example uses the `get()` method on the `'test-db4'` database's
`'foods'` object store to get a single row by the `'name'` primary key:

    import {openDB} from 'idb';

    async function getItemFromStore () {
      const db = await openDB('test-db4', 1);
      const value = await db.get('foods', 'Sandwich');

      console.dir(value);
    }

    getItemFromStore();

Retrieving a single row from the database is fairly straightforward: open
the database and specify the object store and primary key value of the row you
want to get data from. Because the `get()` method returns a promise, you can
`await` it.

> [!NOTE]
> **Note:** If you try to get a nonexistent object, the success handler still executes, but the result is `undefined`.

### Update data

To update data, call the [`put()`](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/put)
method on the object store. The `put()` method is similar to the `add()` method
and can also be used in place of `add()` to create data. Here's a basic example
of using `put()` to update a row in an object store by its primary key value:

    import {openDB} from 'idb';

    async function updateItemInStore () {
      const db = await openDB('example-database', 1);

      // Update a value from in an object store with an inline key:
      await db.put('storeName', { inlineKeyName: 'newValue' });

      // Update a value from in an object store with an out-of-line key.
      // In this case, the out-of-line key value is 1, which is the
      // auto-incremented value.
      await db.put('otherStoreName', { field: 'value' }, 1);
    }

    updateItemInStore();

Like other methods, this method returns a promise. You can also use `put()` as
part of a transaction. Here's an example using the `'foods'` store from earlier
that updates the price of the sandwich and the eggs:

    import {openDB} from 'idb';

    async function updateItemsInStore () {
      const db = await openDB('test-db4', 1);
      
      // Create a transaction on the 'foods' store in read/write mode:
      const tx = db.transaction('foods', 'readwrite');

      // Update multiple items in the 'foods' store in a single transaction:
      await Promise.all([
        tx.store.put({
          name: 'Sandwich',
          price: 5.99,
          description: 'A MORE tasty sandwich!',
          updated: new Date().getTime() // This creates a new field
        }),
        tx.store.put({
          name: 'Eggs',
          price: 3.99,
          description: 'Some even NICER eggs you can cook up!',
          updated: new Date().getTime() // This creates a new field
        }),
        tx.done
      ]);
    }

    updateItemsInStore();

How items get updated depends on how you set a key. If you set a `keyPath`,
each row in the object store is associated with an *inline key* . The preceding
example updates rows based on this key, and when you update rows in this
situation, you'll need to specify that key to update the appropriate item in the
object store. You can also create an *out-of-line key* by setting an
`autoIncrement` as the primary key.

> [!CAUTION]
> **Caution:** When updating a row in an object store, IndexedDB doesn't perform a diff on the data you're updating. For example, if you use `.add()` to add a new row, then update that value later with `.put()`, it erases any fields in the original value that aren't in the new value.

### Delete data

To delete data, call the [`delete()`](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/delete)
method on the object store:

    import {openDB} from 'idb';

    async function deleteItemFromStore () {
      const db = await openDB('example-database', 1);

      // Delete a value 
      await db.delete('storeName', 'primary-key-value');
    }

    deleteItemFromStore();

Like `add()` and `put()`, you can use this as part of a transaction:

    import {openDB} from 'idb';

    async function deleteItemsFromStore () {
      const db = await openDB('test-db4', 1);
      
      // Create a transaction on the 'foods' store in read/write mode:
      const tx = db.transaction('foods', 'readwrite');

      // Delete multiple items from the 'foods' store in a single transaction:
      await Promise.all([
        tx.store.delete('Sandwich'),
        tx.store.delete('Eggs'),
        tx.done
      ]);
    }

    deleteItemsFromStore();

The structure of the database interaction is the same as for the other
operations. Remember to check that the whole transaction has completed by
including the `tx.done` method in the array you pass to `Promise.all`.

## Getting all the data

So far you've only retrieved objects from the store one at a time. You can also
retrieve all of the data, or a subset, from an object store or index using
either the `getAll()` method or cursors.

### The `getAll()` method

The simplest way to retrieve all of an object store's data is to call `getAll()`
on the object store or index, like this:

    import {openDB} from 'idb';

    async function getAllItemsFromStore () {
      const db = await openDB('test-db4', 1);

      // Get all values from the designated object store:
      const allValues = await db.getAll('storeName');

      console.dir(allValues);
    }

    getAllItemsFromStore();

This method returns all the objects in the object store, with no constraints
whatsoever. It's the most direct way of getting all values from an object store,
but also the least flexible.

    import {openDB} from 'idb';

    async function getAllItemsFromStore () {
      const db = await openDB('test-db4', 1);

      // Get all values from the designated object store:
      const allValues = await db.getAll('foods');

      console.dir(allValues);
    }

    getAllItemsFromStore();

This example calls `getAll()` on the `'foods'` object store. This returns all of
the objects from `'foods'`, ordered by the primary key.

### How to use cursors

Cursors are a more flexible way to retrieve multiple objects. A cursor selects
each object in an object store or index one-by-one, letting you do something
with the data when it's selected. Cursors, like the other database operations,
work in transactions.

To create a cursor, call [`openCursor()`](https://developer.mozilla.org/docs/Web/API/IDBObjectStore/openCursor)
on the object store as part of a transaction. Using the `'foods'` store from
previous examples, this is how to advance a cursor through all rows of data in
an object store:

    import {openDB} from 'idb';

    async function getAllItemsFromStoreWithCursor () {
      const db = await openDB('test-db4', 1);
      const tx = await db.transaction('foods', 'readonly');

      // Open a cursor on the designated object store:
      let cursor = await tx.store.openCursor();

      // Iterate on the cursor, row by row:
      while (cursor) {
        // Show the data in the row at the current cursor position:
        console.log(cursor.key, cursor.value);

        // Advance the cursor to the next row:
        cursor = await cursor.continue();
      }
    }

    getAllItemsFromStoreWithCursor();

The transaction in this case is opened in `'readonly'` mode, and its
`openCursor` method is called. In a subsequent `while` loop, the row at the
cursor's current position can have its `key` and `value` properties read, and
you can operate on those values in whatever way makes the most sense for your
app. When you're ready, you can then call the `cursor` object's `continue()`
method to go to the next row, and the `while` loop terminates when the cursor
reaches the end of the dataset.

> [!NOTE]
> **Note:** The `cursor` interface also offers an `advance` method, which takes an argument of how many rows you want to advance the cursor by. For example, calling `cursor.advance(2)` would give you every other row on each iteration.

### Use cursors with ranges and indexes

Indexes let you fetch the data in an object store by a property other than the
primary key. You can create an index on any property, which becomes the `keyPath`
for the index, specify a range on that property, and get the data within the
range using `getAll()` or a cursor.

Define your range using the `IDBKeyRange` object. and any of the following
methods:

- [`upperBound()`](https://developer.mozilla.org/docs/Web/API/IDBKeyRange/upperBound).
- [`lowerBound()`](https://developer.mozilla.org/docs/Web/API/IDBKeyRange/lowerBound).
- [`bound()`](https://developer.mozilla.org/docs/Web/API/IDBKeyRange/bound) (which is both).
- [`only()`](https://developer.mozilla.org/docs/Web/API/IDBKeyRange/only).
- [`includes()`](https://developer.mozilla.org/docs/Web/API/IDBKeyRange/includes).

The `upperBound()` and `lowerBound()` methods specify the upper and lower limits
of the range.

    IDBKeyRange.lowerBound(indexKey);

Or:

    IDBKeyRange.upperBound(indexKey);

They each take one argument: the index's `keyPath` value for the item you want
to specify as the upper or lower limit.

The `bound()` method specifies both an upper and lower limit:

    IDBKeyRange.bound(lowerIndexKey, upperIndexKey);

The range for these functions is inclusive by default, which means it includes
the data that's specified as the limits of the range. To leave out those values,
specify the range as *exclusive* by passing `true` as the second argument for
`lowerBound()` or `upperBound()`, or as the third and fourth arguments of
`bound()`, for the lower and upper limits respectively.

The next example uses an index on the `'price'` property in the `'foods'` object
store. The store now also has a form attached to it with two inputs for the
upper and lower limits of the range. Use the following code to find foods with
prices between those limits:

    import {openDB} from 'idb';

    async function searchItems (lower, upper) {
      if (!lower === '' && upper === '') {
        return;
      }

      let range;

      if (lower !== '' && upper !== '') {
        range = IDBKeyRange.bound(lower, upper);
      } else if (lower === '') {
        range = IDBKeyRange.upperBound(upper);
      } else {
        range = IDBKeyRange.lowerBound(lower);
      }

      const db = await openDB('test-db4', 1);
      const tx = await db.transaction('foods', 'readonly');
      const index = tx.store.index('price');

      // Open a cursor on the designated object store:
      let cursor = await index.openCursor(range);

      if (!cursor) {
        return;
      }

      // Iterate on the cursor, row by row:
      while (cursor) {
        // Show the data in the row at the current cursor position:
        console.log(cursor.key, cursor.value);

        // Advance the cursor to the next row:
        cursor = await cursor.continue();
      }
    }

    // Get items priced between one and four dollars:
    searchItems(1.00, 4.00);

The example code first gets the values for the limits and checks whether the limits
exist. The next block of code decides which method to use to limit the range
based on the values. In the database interaction, open the object store on the
transaction as usual, then open the `'price'` index on the object store. The
`'price'` index lets you search for items by price.

The code then opens a cursor on the index and passes in the range. The cursor
returns a promise representing the first object in the range, or `undefined` if
there's no data within the range. The `cursor.continue()` method returns a
cursor representing the next object, and continues through the loop until you
reach the end of the range.

## Database versioning

When you call the `openDB()` method, you can specify the database version number
in the second parameter. In all the examples in this guide, the version has been
set to `1`, but a database can be upgraded to a new version if you need to
modify it in some way. If the version specified is greater than the version of
the existing database, the `upgrade` callback in the event object executes,
allowing you to add new object stores and indexes to the database.

The `db` object in the `upgrade` callback has a special `oldVersion` property,
which indicates the version number of the database the browser has access to.
You can pass this version number into a `switch` statement to execute blocks of
code inside the `upgrade` callback based on the existing database version
number. Here's an example:

    import {openDB} from 'idb';

    const db = await openDB('example-database', 2, {
      upgrade (db, oldVersion) {
        switch (oldVersion) {
          case 0:
            // Create first object store:
            db.createObjectStore('store', { keyPath: 'name' });

          case 1:
            // Get the original object store, and create an index on it:
            const tx = await db.transaction('store', 'readwrite');
            tx.store.createIndex('name', 'name');
        }
      }
    });

This example sets the newest version of the database to `2`. When this code
first executes, the database doesn't yet exist in the browser, so `oldVersion`
is `0`, and the `switch` statement starts at `case 0`. In the example, this
adds a `'store'` object store to the database.

Key point: In `switch` statements, there's usually a `break` after each `case`
block, but this is deliberately not used here. This way, if the existing
database is a few versions behind, or if it doesn't exist, the code continues
through the rest of the `case` blocks until it's up to date. So in the example,
the browser continues executing through `case 1`, creating a `name` index on the
`store` object store.

To create a `'description'` index on the `'store'` object store, update the
version number and add a new `case` block as follows:

    import {openDB} from 'idb';

    const db = await openDB('example-database', 3, {
      upgrade (db, oldVersion) {
        switch (oldVersion) {
          case 0:
            // Create first object store:
            db.createObjectStore('store', { keyPath: 'name' });

          case 1:
            // Get the original object store, and create an index on it:
            const tx = await db.transaction('store', 'readwrite');
            tx.store.createIndex('name', 'name');

          case 2:
            const tx = await db.transaction('store', 'readwrite');
            tx.store.createIndex('description', 'description');
        }
      }
    });

If the database you created in the previous example still exists in the browser,
when this executes, `oldVersion` is `2`. The browser skips `case 0` and
`case 1`, and executes the code in `case 2`, which creates a `description`
index. After that, the browser has a database at version 3 containing a `store`
object store with `name` and `description` indexes.

## Further reading

The following resources provide more information and context for using IndexedDB.

### IndexedDB documentation

- [`idb` Github repository](https://github.com/jakearchibald/idb)
- [Using IndexedDB](https://developer.mozilla.org/docs/Web/API/IndexedDB_API/Using_IndexedDB)
- [Basic Concepts Behind IndexedDB](https://developer.mozilla.org/docs/Web/API/IndexedDB_API/Basic_Concepts_Behind_IndexedDB)
- [Indexed Database API 3.0 specification](https://www.w3.org/TR/IndexedDB/)

### Data storage limits

- [Storage for the web](https://web.dev/articles/storage-for-the-web)
- [Browser storage limits and eviction criteria](https://developer.mozilla.org/docs/Web/API/IndexedDB_API/Browser_storage_limits_and_eviction_criteria)