IndexedDB Versioning and Upgrade Mechanism Explained

IndexedDB uses a strict, version-based schema management system in JavaScript to handle changes to database structures over time. Whenever you need to create object stores, modify indexes, or restructure existing data, you must increment the database version number. This triggers a dedicated upgradeneeded event, which runs a special schema-modifying transaction that ensures database changes are applied safely, atomically, and without corrupting existing records.

Database Versioning Basics

In IndexedDB, database versions are represented as positive integers (e.g., 1, 2, 3). Floating-point numbers are converted to integers, and setting a version lower than the current version will result in an error.

You request a specific version when opening a database:

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

When this line executes, the browser compares the requested version with the database’s existing version on disk: - If the database does not exist, the browser treats the current version as 0 and fires the upgrade process to reach version 2. - If the database exists and the requested version is higher than the current version, an upgrade is triggered. - If the requested version matches the existing version, the database opens normally via the onsuccess event. - If the requested version is lower, an onerror event is fired with a VersionError.

The onupgradeneeded Event and versionchange Transaction

Structural modifications—such as calling createObjectStore, deleteObjectStore, createIndex, or deleteIndex—cannot be performed during standard read or write operations. They are only permitted inside the onupgradeneeded event handler.

When onupgradeneeded fires, the browser automatically creates an exclusive, system-managed transaction of type versionchange. Key characteristics of this transaction include:

  1. Exclusive Lock: It blocks all other connections and transactions across all browser tabs for that database until the upgrade finishes or fails.
  2. Atomicity: If an unhandled exception occurs or transaction.abort() is called, all changes made during the upgrade are rolled back, and the database version reverts to its previous state.
  3. Data Migration Access: The versionchange transaction allows both schema updates and standard data read/write operations, making it possible to migrate or transform existing records during the upgrade.

Managing Incremental Migrations

The IDBVersionChangeEvent object passed to onupgradeneeded provides two useful properties: event.oldVersion and event.newVersion.

To handle users upgrading from different older versions, use conditional logic (such as a switch statement without break statements, or simple if checks) to apply migrations sequentially:

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 index to the existing store
  if (oldVersion < 2) {
    const transaction = event.target.transaction;
    const userStore = transaction.objectStore("users");
    userStore.createIndex("by_name", "name", { unique: false });
  }

  // Version 3: Create an entirely new store
  if (oldVersion < 3) {
    db.createObjectStore("settings", { keyPath: "key" });
  }
};

request.onsuccess = (event) => {
  const db = event.target.result;
  console.log("Database opened successfully at version:", db.version);
};

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

Handling Blocked Upgrades and Multi-Tab Conflicts

Because an upgrade transaction requires exclusive access to the database, an upgrade cannot proceed if another tab or window has an open connection to an older version of the same database.

IndexedDB provides two events to manage this scenario:

To ensure smooth upgrades across multiple tabs, always listen for onversionchange on your database instances and close them promptly:

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

  db.onversionchange = () => {
    db.close();
    alert("A new version of the app is available. Please reload the page.");
  };
};

request.onblocked = () => {
  console.warn("Upgrade blocked: Please close other tabs running this app.");
};