IndexedDB onupgradeneeded and Schema Migrations

This article provides an in-depth look at the onupgradeneeded event in IndexedDB and explains how JavaScript manages database schema migrations. You will learn what triggers the onupgradeneeded lifecycle event, how IndexedDB versioning works, and the standard architectural patterns for creating object stores, updating indexes, and safely transforming existing data across different database versions.

What is the onupgradeneeded Event?

IndexedDB is a low-level, transactional client-side database built directly into modern web browsers. Unlike relational databases that use SQL statements for schema alterations, IndexedDB controls schema changes through a strictly defined lifecycle event called onupgradeneeded.

The onupgradeneeded event is the only place in an application’s lifecycle where you are permitted to alter the structure of an IndexedDB database. Within this event handler, you can:

Attempting to modify database structures outside of this event will throw an InvalidStateError.

How IndexedDB Versioning Works

When you open a connection to an IndexedDB database, you pass a database name and an optional integer version number:

const request = indexedDB.open("AppDatabase", 2);

IndexedDB handles the connection request based on the requested version versus the version already stored on the user’s device:

  1. New Database Creation: If the database does not exist, the browser considers its initial version to be 0. Opening it with version 1 triggers onupgradeneeded.
  2. Version Upgrade: If the database exists at version 1 and you request version 2, onupgradeneeded fires before the database opens.
  3. Same Version: If the requested version matches the stored version, onsuccess fires immediately, skipping onupgradeneeded.
  4. Version Downgrade: If you request a version lower than the stored version, the onblocked or onerror event fires with a VersionError. IndexedDB does not natively support downgrading.

Handling Database Migrations

Because client-side applications cannot guarantee that a user visits every single release sequentially, a user may jump directly from version 1 to version 3. Your migration logic must be able to handle incremental schema updates dynamically.

Within the onupgradeneeded event object, the event.oldVersion property indicates the database version currently stored on the client’s machine, and event.newVersion indicates the target version.

Sequential Migration Pattern

The standard way to handle migrations in JavaScript is using a switch statement without break statements (fall-through pattern) or sequential if blocks evaluated against event.oldVersion.

const request = indexedDB.open("AppDatabase", 3);

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

  // Version 1: Initial setup
  if (oldVersion < 1) {
    const userStore = db.createObjectStore("users", { keyPath: "id" });
    userStore.createIndex("by_email", "email", { unique: true });
  }

  // Version 2: Add a new object store and an index
  if (oldVersion < 2) {
    const orderStore = db.createObjectStore("orders", { keyPath: "orderId" });
    orderStore.createIndex("by_user", "userId", { unique: false });
  }

  // Version 3: Add a new index to an existing store
  if (oldVersion < 3) {
    const transaction = event.target.transaction;
    const userStore = transaction.objectStore("users");
    userStore.createIndex("by_name", "name", { unique: false });
  }
};

request.onsuccess = (event) => {
  const db = event.target.result;
  console.log("Database successfully opened and ready for transactions.");
};

request.onerror = (event) => {
  console.error("Database failed to open:", event.target.error);
};

Migrating and Transforming Stored Data

Schema changes sometimes require updating the shape of existing data records. When onupgradeneeded is triggered, an implicit version-change transaction (IDBTransaction) is automatically created and accessible via event.target.transaction.

You can use this transaction to iterate through existing records with a cursor and update fields:

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

  if (event.oldVersion < 2) {
    const userStore = transaction.objectStore("users");
    
    // Iterate over all records to add a new default property
    userStore.openCursor().onsuccess = (cursorEvent) => {
      const cursor = cursorEvent.target.result;
      if (cursor) {
        const updatedRecord = cursor.value;
        updatedRecord.isActive = true; // Add new property
        
        cursor.update(updatedRecord);
        cursor.continue();
      }
    };
  }
};

Transaction Rollback and Error Handling

All structural operations and data modifications executed inside the onupgradeneeded handler run within a single atomic transaction. If an error occurs during any step of the migration process, the browser automatically aborts the transaction. This rolls the database back to its previous version and state, preventing partial updates and database corruption.