Chapter 49: IndexedDB Client-Side Database

ObjectStores & Primary Keys

Designing schema containers, mastering in-line vs out-of-line keys, auto-increment generators, and composite key paths.

LEARNING OBJECTIVES
  • Understand the role of an IDBObjectStore as the primary storage collection in IndexedDB.
  • Differentiate between the four fundamental keying strategies in IndexedDB.
  • Implement composite (multi-field) primary keys using array keyPath definitions.
  • Identify valid vs invalid IndexedDB key data types according to the W3C specification.
🎬 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 a modern warehouse fulfillment center.

An ObjectStore is like a specialized storage aisle in the warehouse (for example, the "Electronics Aisle" or the "Customer Accounts Aisle"). Every item placed on the shelves must have an inventory tracking identifier—a Primary Key—so workers can instantly pinpoint and retrieve it.

How do items get their inventory tracking numbers?

  1. The In-Line Barcode (keyPath: "sku"): The manufacturer prints the barcode directly onto the product's packaging. When you receive the product, the warehouse system reads the existing sku property inside the object itself.
  2. The In-Line Smart Stamper (keyPath: "id", autoIncrement: true): The product arrives as a blank box without an ID. The receiving machine automatically laser-prints an incremental number (1, 2, 3...) right onto the product and records that number in the object's id field.
  3. The Out-of-Line Shelf Tag (autoIncrement: false, no keyPath): The items are plain unmarked goods. The warehouse manager manually writes an external tag on the shelf slot (e.g., "slot-A9") during delivery (store.add(item, "slot-A9")). The item itself has no clue what slot it sits in.
  4. The Out-of-Line Auto-Ticket Machine (autoIncrement: true, no keyPath): Unmarked goods enter the conveyor belt, and a ticket dispenser hands the worker a sequential receipt number (1, 2, 3...) while placing the item on the shelf.

Technical Deep Dive & Specifications

The Four Primary Keying Strategies

When creating an ObjectStore using db.createObjectStore(storeName, options), you configure how keys are resolved for every record:

+----------------------------------------------------------------------------------------------------+
|                                INDEXEDDB KEYING STRATEGY MATRIX                                    |
+-------------------+---------------------+-------------------------+--------------------------------+
| Key Strategy      | createObjectStore   | How Key is Determined   | Example store.add() Call       |
|                   | Options             |                         |                                |
+-------------------+---------------------+-------------------------+--------------------------------+
| 1. In-Line Only   | { keyPath: "id" }   | Extracted directly from | `store.add({ id: "u_1", ...})` |
|                   |                     | the object property     | (Fails if 'id' is missing)     |
+-------------------+---------------------+-------------------------+--------------------------------+
| 2. In-Line + Auto | { keyPath: "id",    | If object has 'id', it  | `store.add({ name: "Dan" })`   |
|    Increment      |   autoIncrement:    | is used; otherwise,     | (Auto-assigns id: 1 to object) |
|                   |   true }            | generator creates one   |                                |
+-------------------+---------------------+-------------------------+--------------------------------+
| 3. Out-of-Line    | { }                 | Must pass key as second | `store.add({ name: "Eva" },    |
|    Manual         | (no options)        | parameter in add()/put()|            "user_eva_99")`     |
+-------------------+---------------------+-------------------------+--------------------------------+
| 4. Out-of-Line    | { autoIncrement:    | Generator creates key;  | `store.add({ name: "Frank" })` |
|    Auto Increment |   true }            | returns key on success  | (Returns key 1; object unmod.) |
+-------------------+---------------------+-------------------------+--------------------------------+

Valid vs Invalid Key Types

IndexedDB enforces strict rules regarding what JavaScript values qualify as valid keys:

+---------------------------------------------------------------------------------+
|                               VALID KEY TYPES                                   |
+---------------------------------------------------------------------------------+
| ✅ String         | Any valid UTF-16 string (e.g. "order_9812", "[email protected]") |
| ✅ Number         | Any finite IEEE 754 number (e.g. 1, 42.5). NOT NaN or Infinity!|
| ✅ Date           | Valid Date objects (e.g. new Date()). NOT Invalid Date!       |
| ✅ ArrayBuffer    | Binary buffer instances (or TypedArray views)                   |
| ✅ Array          | Arrays where EVERY element is itself a valid key (Compound Key) |
+---------------------------------------------------------------------------------+
|                              INVALID KEY TYPES                                  |
+---------------------------------------------------------------------------------+
| ❌ Boolean        | true, false (Throws DataError)                                  |
| ❌ Null / Undef   | null, undefined (Throws DataError)                              |
| ❌ Plain Object   | { foo: "bar" } cannot be a key (Throws DataError)               |
| ❌ RegExp / Map   | Non-primitive collections and regular expressions               |
+---------------------------------------------------------------------------------+

Compound (Composite) Primary Keys

You can define a composite primary key by passing an array of property paths to keyPath:

// Compound key: Unique combination of tenantId and invoiceNumber
const store = db.createObjectStore('invoices', { 
  keyPath: ['tenantId', 'invoiceNumber'] 
});

// Storing a record:
store.add({
  tenantId: 'acme_corp',
  invoiceNumber: 10452,
  amount: 4500.00
});
// Key in store is evaluated as: ['acme_corp', 10452]

💻 Interactive Code Playground

Starter Code

Save this file as object-stores.html and open it in your browser.

Line-by-Line Code Breakdown

  • Line 55 (d.createObjectStore('articles', { keyPath: 'id', autoIncrement: true })): Generates an auto-incrementing integer key and writes it directly into the object's id property.
  • Line 57 (d.createObjectStore('blobs')): Creates an out-of-line store. Every insertion requires passing the key as the second parameter: store.add(data, key).
  • Line 59 (d.createObjectStore('metrics', { keyPath: ['server', 'timestamp'] })): Configures a compound key. An entry is valid only if both server and timestamp contain valid key data types.
  • Lines 70–75 (store.add(article)): Notice that when autoIncrement is combined with keyPath: 'id', IndexedDB mutates the in-memory object by attaching the generated id.
  • Lines 104–109 (store.add({ data: 1 }, true)): Demonstrates that passing a Boolean (true) as a primary key violates the IndexedDB specification and immediately throws a DataError.

Expected Browser Render Output


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:40:01] 🛠️ Creating 3 distinct ObjectStores...
[02:40:01] ✅ KeySchemesDB ready for testing.
[02:40:05] 📰 Article inserted! Generated key: 1, Object id: 1
[02:40:08] 📦 Blob stored with out-of-line key: "asset_logo_1718000100"
[02:40:11] 📈 Compound Metric stored! Key: ["us-east-1", 1718000103]
[02:40:14] ⚠️ Attempting store.add({ data: 1 }, true) [Boolean Key]...
[02:40:14] ❌ Caught Expected Error: DataError - The data provided to an operation does not meet requirements.

🏋️ Hands-On Exercise

🎯 The Challenge: Multi-Tenant Schema Architect

Instructions:

  1. Create an IndexedDB database named SaaSPlatform at Version 1.
  2. In onupgradeneeded, create an ObjectStore named tenant_files configured with:
    • A compound primary key consisting of tenantId and filePath.
  3. Insert two records for tenantId: "tenant_alpha":
    • { tenantId: "tenant_alpha", filePath: "/docs/readme.txt", size: 1024 }
    • { tenantId: "tenant_alpha", filePath: "/images/hero.png", size: 204800 }
  4. Attempt to insert a duplicate record with the exact same tenantId and filePath and verify that IndexedDB triggers a ConstraintError.

🏁 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. Supplying an Out-of-Line Key to an In-Line Store: If a store was defined with keyPath: "id", executing store.add({ id: 1, name: "A" }, 1) throws an InvalidAccessError: Failed to execute 'add' on 'IDBObjectStore': The object store uses in-line keys and the key parameter was provided.
  2. Using Invalid Key Types in keyPath: Attempting to use true, false, null, or an empty object as a primary key throws a DataError.
  3. Modifying In-Line Key Properties Post-Creation: If you update an object and change its keyPath value, calling store.put(updatedObj) will insert a brand new record instead of updating the existing one.

💡 Pro Tips

  1. Leverage Composite Keys for Hierarchical Isolation: In multi-tenant, workspace-based, or folder-based web apps, using compound keys like ['workspaceId', 'documentId'] provides instant tenant isolation without requiring separate ObjectStores.
  2. Use UUIDs / CUIDs for Distributed Offline Systems: When building offline-first apps that sync with a server, prefer client-generated UUIDv4 or ULID strings over autoIncrement: true to prevent ID collisions during multi-device synchronization.

📌 Key Takeaways

  • An IDBObjectStore is the core container in IndexedDB, holding records organized by primary keys.
  • In-line keys extract the primary key from an internal property (keyPath), while out-of-line keys are passed as explicit arguments.
  • autoIncrement: true instructs the browser engine to generate sequential numeric keys automatically.
  • Compound keys (keyPath: ['fieldA', 'fieldB']) enforce uniqueness across multiple properties.
  • Numbers, strings, Dates, ArrayBuffers, and Arrays of valid keys are legal key types; Booleans, objects, and null values are illegal.
  • --
⭐ LEARN: HTML 🌟 ⚔️ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

What error is thrown if you pass an explicit key argument to store.add(data, "key123") on an ObjectStore configured with { keyPath: "id" }?

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

Which of the following JavaScript values is a VALID primary key in IndexedDB?

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

How does setting autoIncrement: true behave when combined with keyPath: "id"?

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