Chapter 2
IndexedDB Data Model and Architecture
Unlock the inner mechanics of IndexedDB with an architect's eye. This chapter goes beneath the API surface to expose the foundational data structures, persistence guarantees, and security boundaries that make IndexedDB a robust engine for modern web data. Discover how schema evolution, indexing strategies, and transactional integrity converge to empower the next generation of performant, resilient client-side applications.
2.1 Databases, Object Stores, and Schemas
IndexedDB operates as a client-side storage system based on a hierarchical logical framework: a database serves as the top-level container, encompassing multiple object stores which in turn hold the actual data records. This architecture supports complex structured storage and efficient retrieval of large volumes of data within web applications, extending beyond simpler key-value paradigms.
At its core, an IndexedDB database is identified by a unique name and version number, allowing explicit control over the evolution of its schema. This versioning is critical, as databases are not mutable arbitrarily; instead, structural changes necessitate creating a new schema version, which triggers a dedicated upgrade transaction. This transaction provides a controlled environment to modify the database layout, such as adding or deleting object stores, indexes, or changing key-path definitions.
Each object store can be conceptualized as a specialized persistent collection, analogous to a table in relational databases but without rigid schemas for each record. Object stores contain records-JavaScript objects-that must include a unique primary key. The primary key can be explicit, generated automatically, or derived from a property or nested property specified by a key path. This key path is a string or an array of strings delineating where within the stored object the key value is found. For example, the key path user.id accesses the id property inside a nested user object.
Key-path-based schema definition is essential both for uniqueness enforcement and efficient data access. By indexing data based on key paths, IndexedDB enables complex querying patterns beyond mere key lookups, including range queries and multi-entry indexes. Best practices suggest carefully selecting key paths that reflect natural keys-for instance, usernames or universally unique identifiers (UUIDs)-to facilitate logical data access and minimize update complexity. In addition, choosing appropriate key types (strings, numbers, Date objects) that align with the application's domain model ensures compatibility and performance.
Schema evolution in IndexedDB revolves around the database version number. When an application detects a change in schema requirements, it must open the database with an incremented version. This action fires an onupgradeneeded event, providing access to the currently open database instance within an upgrade transaction. Within this transition, developers can create new object stores, delete obsolete ones, or alter indexes. However, IndexedDB does not support in-place modification of existing object stores or indexes; the prescribed method is to delete and recreate them with the new schema definitions.
This controlled upgrade model has several critical implications. Firstly, all schema migration logic is constrained to the upgrade transaction, which must be synchronous with respect to the user's database connection attempts. Long-running migrations can block concurrent access and lead to performance degradation or contention. Therefore, schema changes should be as granular and incremental as possible. Secondly, data migration between old and new store structures must be explicitly programmed by iterating existing records and copying them into the new stores or indexes where necessary.
To design robust object stores, it is advisable to:
- Define a clear primary key based on natural, immutable identifiers using key paths to avoid key collisions and maintain data integrity.
- Use indexes sparingly and purposefully-indexes implicitly consume storage space and impose overhead on write operations. Each index should support a distinct query pattern justified by application logic.
- Ensure that complex or nested data structures stored within object stores are serializable as structured clones to guarantee reliable persistence across browsing sessions.
- Version object store schemas with a forward-compatible mindset, preferring additive changes over destructive ones when possible.
Consider an example where an application maintains an object store named contacts:
const request = indexedDB.open('addressBook', 2); request.onupgradeneeded = event => { const db = event.target.result; if (!db.objectStoreNames.contains('contacts')) { const store = db.createObjectStore('contacts', { keyPath: 'email' }); store.createIndex('lastName', 'name.last', { unique: false }); } }; Here, the object store contacts uses the email property as the primary key, ensuring uniqueness by leveraging a logical business identifier. An index on the nested name.last property enables efficient queries on last names. In case a new version adds a phoneNumber property indexed for quick lookup, the upgrade procedure would create the new index inside a new version upgrade event.
Pragmatically, the necessity to migrate schema versions means that web applications must incorporate robust version negotiation and error handling strategies. Users opening a database with a higher version number must be prepared to execute upgrade logic, while users running outdated application versions might encounter incompatible schemas. It is therefore recommended to maintain precise version control and document schema changes thoroughly to manage these transitions safely.
The IndexedDB logical framework-comprising ...