Skip to content

Migrations and Blobs

Every IndexedDB database has an integer version number. When you call indexedDB.open(name, version) with a version higher than the one currently stored, the browser fires the upgradeneeded event before success. This is the only place you can create or delete object stores and indexes.

const request = indexedDB.open('my-db', 2);
request.onupgradeneeded = (event) => {
const db = event.target.result;
const oldVersion = event.oldVersion; // 0 if brand-new, 1 if upgrading from v1
// ...create stores based on oldVersion
};

The key property is event.oldVersion: it tells you what version the database was before this open call. A brand-new database reports oldVersion === 0.

The migration pattern: cascading if (oldVersion < N) checks

Section titled “The migration pattern: cascading if (oldVersion < N) checks”

Rather than a single if/else, use cascading less-than checks so a database at any previous version runs all the upgrades it missed:

request.onupgradeneeded = (event) => {
const db = event.target.result;
const { oldVersion } = event;
if (oldVersion < 1) {
// v0 → v1: create the initial 'users' store
db.createObjectStore('users', { keyPath: 'id' });
}
if (oldVersion < 2) {
// v1 → v2: add a 'files' store for binary data
db.createObjectStore('files', { keyPath: 'name' });
}
// if oldVersion < 3: future migration goes here
};

A user upgrading from v0 runs both blocks. A user upgrading from v1 runs only the second block. A user already on v2 runs neither.

One of IndexedDB’s biggest advantages over Web Storage is its ability to store structured-cloneable JavaScript values — including Blob and File objects — without any serialisation.

// Create a Blob in memory
const blob = new Blob(['Hello, binary world!'], { type: 'text/plain' });
// Store it directly — no JSON.stringify, no base64 encoding
const tx = db.transaction('files', 'readwrite');
tx.objectStore('files').put({ name: 'greeting.txt', data: blob });

When you read the record back, you get a real Blob with all its methods intact:

const getReq = db.transaction('files').objectStore('files').get('greeting.txt');
getReq.onsuccess = async () => {
const record = getReq.result;
const text = await record.data.text(); // Blob.prototype.text() returns a Promise
console.log(text); // 'Hello, binary world!'
};

The same technique works for File objects (which are a subclass of Blob), ArrayBuffer, ImageBitmap, and other structured-cloneable types.

The demo below opens demo-advanced-migrations at version 2. If the database does not exist yet, onupgradeneeded runs both migration blocks (oldVersion is 0). It then stores a user record and a Blob, retrieves the Blob, and reads its text content before cleaning up.

Browser Storage
ConceptDetail
event.oldVersionInteger version before this open; 0 for a brand-new database
Cascading if (oldVersion < N)Ensures incremental migrations are never skipped
Blob / File in IDBStored and retrieved without any serialisation
Blob.prototype.text()Returns a Promise<string> — use await
What value does event.oldVersion hold when a user opens an IndexedDB database for the very first time (it has never existed before)?
You open a database at version 3. The user currently has version 1. Which migration blocks should run inside onupgradeneeded?
How should you store a Blob object in IndexedDB?
What does Blob.prototype.text() return?