Migrations and Blobs
Schema versioning in IndexedDB
Section titled “Schema versioning in IndexedDB”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.
Storing Blob and File objects
Section titled “Storing Blob and File objects”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 memoryconst blob = new Blob(['Hello, binary world!'], { type: 'text/plain' });
// Store it directly — no JSON.stringify, no base64 encodingconst 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.
Runnable: v2 migration + Blob round-trip
Section titled “Runnable: v2 migration + Blob round-trip”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.
Summary
Section titled “Summary”| Concept | Detail |
|---|---|
event.oldVersion | Integer version before this open; 0 for a brand-new database |
Cascading if (oldVersion < N) | Ensures incremental migrations are never skipped |
Blob / File in IDB | Stored and retrieved without any serialisation |
Blob.prototype.text() | Returns a Promise<string> — use await |