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:
- Create or delete object stores (the IndexedDB equivalent of tables).
- Create or delete indexes.
- Transform or migrate existing data records to conform to a new schema.
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:
- New Database Creation: If the database does not
exist, the browser considers its initial version to be
0. Opening it with version1triggersonupgradeneeded. - Version Upgrade: If the database exists at version
1and you request version2,onupgradeneededfires before the database opens. - Same Version: If the requested version matches the
stored version,
onsuccessfires immediately, skippingonupgradeneeded. - Version Downgrade: If you request a version lower
than the stored version, the
onblockedoronerrorevent fires with aVersionError. 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.