Chapter 49: IndexedDB Client-Side Database

Database Lifecycle & Versioning

Orchestrating database connections, handling schema migrations with `onupgradeneeded`, and synchronizing multi-tab version upgrades.

LEARNING OBJECTIVES
  • Master the database opening workflow with window.indexedDB.open(name, version).
  • Understand the role and execution guarantees of the onupgradeneeded lifecycle event.
  • Implement robust version migration scripts supporting step-by-step schema evolution.
  • Handle multi-tab upgrade contention using onversionchange and onblocked event hooks.
🎬 INTERACTIVE VISUAL PIPELINE Core Architecture Simulation
🌐
1. Input
Directives & Tags
⚙️
2. Parse
Tokenizer & AST
🌳
3. Layout
Box Model & Flow
🎨
4. Render
GPU Paint & Composite
PHASE 1: INPUT & DIRECTIVES
Browser receives declarative markup stream, parsing tag tokens and initializing component state.

📖 The Mental Model & Story (Intuitive Foundation)

Imagine an apartment building that needs a structural renovation—say, adding a new elevator shaft (a schema upgrade).

  1. The Architecture Blueprint (indexedDB.open(name, version)): When you file a building permit with a higher version number (e.g., upgrading from Version 1 to Version 2), the city inspection team checks whether any residents are currently inside the building.
  2. The Construction Zone (onupgradeneeded): If the building permit is approved, the construction crew halts normal tenant activity and opens an exclusive construction phase. In this special window, you are allowed to knock down walls (delete old object stores), build new rooms (create new object stores), or install directory boards (create indexes). Once construction is finished, the zone is sealed.
  3. The Occupancy Certificate (onsuccess): Once the dust clears, tenants (your application's read/write transactions) are welcomed back in to conduct normal daily business.
  4. The Squatter Conflict (onblocked & onversionchange): If another browser tab is currently sitting open with an older Version 1 lease, the construction crew cannot start. The new tab receives an onblocked warning, and the old tab receives an onversionchange notice telling it: "Please close your connection immediately so the renovation can begin!"

Technical Deep Dive & Specifications

The indexedDB.open() State Machine

In IndexedDB, schema alterations (creating or deleting ObjectStores and Indexes) can only occur inside a dedicated versionchange transaction triggered during onupgradeneeded.

                            indexedDB.open(name, version)
                                         |
                                         v
                         +-------------------------------+
                         | Database exists & requested   |
                         | version > current version?    |
                         +-------------------------------+
                                    /         \
                             YES   /           \  NO (Version matches)
                                  v             v
                    +--------------------+    +--------------------+
                    | onupgradeneeded    |    | onsuccess          |
                    | (Schema Migration) |    | (Ready for CRUD)   |
                    +--------------------+    +--------------------+
                              |
                              v (Migration completed)
                    +--------------------+
                    | onsuccess          |
                    | (Ready for CRUD)   |
                    +--------------------+

Versioning Rules & Constraints

  • Version format: Database versions must be unsigned positive integers (e.g., 1, 2, 3). You cannot use decimals (1.5), strings ("2.0"), or negative numbers (-1). Passing 0 or a float throws a TypeError.
  • Implicit Version 1: If you call indexedDB.open("myDB") without a version parameter and the database does not exist, the browser opens it with version 1.
  • Downgrades Forbidden: If the existing database on disk is at version 3 and you call indexedDB.open("myDB", 2), the open request immediately fires an onerror event with a VersionError DOMException.

Multi-Tab Upgrade Coordination

Because an IndexedDB database is shared across all tabs and workers of the same origin, version upgrades require coordination across active connections:

+------------------------------------+          +------------------------------------+
|               TAB A                |          |               TAB B                |
|  (Holds open DB Connection v1)     |          |  (Requests DB Upgrade to v2)       |
+------------------------------------+          +------------------------------------+
                  |                                                |
                  |                                                |-- indexedDB.open("app", 2)
                  |                                                |
                  |<==== onversionchange fired ====================| (Tab B is waiting...)
                  |                                                |
                  |                                                |-- onblocked fired!
                  | (Tab A must call db.close())                   |   (If Tab A doesn't close)
                  |                                                |
                  |-- db.close()                                   |
                  |                                                |
                  |===============================================>| (Lock released!)
                  |                                                |
                  |                                                |-- onupgradeneeded fires
                  |                                                |-- onsuccess fires
  1. Tab B calls open("app", 2).
  2. The browser fires db.onversionchange on Tab A's existing database instance.
  3. If Tab A immediately calls db.close(), Tab B proceeds to onupgradeneeded.
  4. If Tab A fails to close its connection, Tab B's open request fires request.onblocked, warning the user or delaying the migration until Tab A is closed.

💻 Interactive Code Playground

Starter Code

Save this file as lifecycle.html and open it in your browser.

Line-by-Line Code Breakdown

  • Line 57 (const request = indexedDB.open(DB_NAME, version)): Initiates an asynchronous request to open the database. Returns an IDBOpenDBRequest instance.
  • Lines 59–74 (request.onupgradeneeded): Fires if the database is being created for the first time or if the requested version is strictly higher than the database version stored on disk.
  • Lines 67–73 (if (oldVer < 1) ... if (oldVer < 2)): The canonical pattern for sequential, incremental database migrations without destroying existing tenant data.
  • Lines 76–87 (request.onsuccess): Fires once the database is fully open and ready for normal read/write transactions.
  • Lines 81–86 (currentDb.onversionchange): Hooks into the connection to receive eviction notices when another tab requests an upgrade. Closing the database immediately unblocks the requesting tab.
  • Lines 89–91 (request.onblocked): Handles the condition where another tab is holding the database open and refuses to disconnect.
  • Lines 105–112 (indexedDB.deleteDatabase(DB_NAME)): Permanently wipes the database and all its ObjectStores from disk storage.

Expected Browser Render Output

(If you then click "Upgrade to Version 2"):


SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL playground.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...
[02:35:10] ▶️ Requesting connection to "LifecycleDemoDB" at version 1...
[02:35:10] 🛠️ onupgradeneeded: Migrating from version 0 ➔ 1
[02:35:10]    Creating ObjectStore: "users" (keyPath: "id")
[02:35:10] ✅ onsuccess: Connected to "LifecycleDemoDB" (Active Version: 1)
[02:35:15] ▶️ Requesting connection to "LifecycleDemoDB" at version 2...
[02:35:15] ⚠️ onversionchange: Another tab wants to upgrade this database. Closing our connection...
[02:35:15] 🔒 Connection closed gracefully.
[02:35:15] 🛠️ onupgradeneeded: Migrating from version 1 ➔ 2
[02:35:15]    Creating ObjectStore: "settings" (autoIncrement: true)
[02:35:15] ✅ onsuccess: Connected to "LifecycleDemoDB" (Active Version: 2)

🏋️ Hands-On Exercise

🎯 The Challenge: Resilient Multi-Version Migration Runner

Instructions:

  1. Construct an IndexedDB helper function initStore(targetVersion) that connects to a database named EnterpriseStore.
  2. Implement schema migrations across 3 versions:
    • Version 1: Creates store "products" with { keyPath: "sku" }.
    • Version 2: Creates store "orders" with { keyPath: "orderId" }.
    • Version 3: Deletes the deprecated "products" store and creates "inventory" with { keyPath: "itemId", autoIncrement: true }.
  3. Add full error handling for version downgrades, connection blocking, and database deletion.

🏁 Starter Code Sandbox

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
STARTER CODE SANDBOX exercise.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...

⚠️ Common Pitfalls

  1. Creating ObjectStores outside onupgradeneeded: Calling db.createObjectStore() inside onsuccess throws an InvalidStateError: Failed to execute 'createObjectStore' on 'IDBDatabase': The database is not running a version change transaction.
  2. Forgetting to Handle onversionchange: If your app does not attach a db.onversionchange listener, a user opening a newly deployed version of your app in a second tab will hang indefinitely in onblocked.
  3. Passing Float Versions: Executing indexedDB.open("db", 2.1) rounds or throws errors depending on the engine. Always increment version numbers as pure integers (1, 2, 3).

💡 Pro Tips

  1. Never Downgrade Schemas in Production: IndexedDB strictly rejects version decreases. If a breaking schema bug occurs, push an emergency release that increments to version N + 1 with a repair migration script.
  2. Track Migration History: For enterprise offline apps, store a schema_migrations record within IndexedDB containing timestamps, previous version snapshots, and migration diagnostics for telemetry reporting.

📌 Key Takeaways

  • Schema modifications can only take place inside the onupgradeneeded lifecycle handler.
  • Versions must be positive non-zero integers (1, 2, 3...); downgrading versions throws a VersionError.
  • Multi-step schema upgrades should use cumulative if (oldVersion < X) conditional blocks.
  • Every active database instance should register an onversionchange handler to close its connection when another tab upgrades the schema.
  • The onblocked handler alerts the application when an upgrade cannot proceed due to lingering open connections in other tabs.
  • --
⭐ LEARN: HTML 🌟 ⚔️ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

What happens if a database is currently at Version 3, and a script executes indexedDB.open('myDB', 2)?

Question 1 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 2 / 3

Where is the ONLY valid place in JavaScript to execute db.createObjectStore() or db.deleteObjectStore()?

Question 2 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 3 / 3

What is the purpose of the db.onversionchange event listener on an active IDBDatabase connection?

Question 3 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP