DataFn
Storage

IndexedDB

Persistent browser storage via IndexedDB.

The IndexedDbStorageAdapter provides persistent client-side storage backed by the browser IndexedDB API. It is the recommended adapter for production browser applications.

Creating an Adapter

The recommended way to create the adapter is with a schema, which ensures all resource object stores and join tables are created in IndexedDB:

import { IndexedDbStorageAdapter } from "@datafn/client";

const storage = IndexedDbStorageAdapter.create({
  dbName: "my-app-db",
  schema,
});

Multi-Tenant / Multi-User Isolation

For multi-user applications, use createForUser to create isolated databases per user and tenant:

// Single-tenant
const storage = IndexedDbStorageAdapter.createForUser(
  "my-app",
  userId,
  undefined,
  undefined,
  schema,
);
// Database name: "my-app_<userId>"

// Multi-tenant
const storage = IndexedDbStorageAdapter.createForUser(
  "my-app",
  userId,
  tenantId,
  undefined,
  schema,
);
// Database name: "my-app_<tenantId>_<userId>"

Schema-Aware Store Creation

When a schema is provided, the adapter automatically:

  1. Creates an object store for each resource (keyed by id).
  2. Creates indices from the resource schema (e.g., fields listed in indices).
  3. Creates foreign key indices inferred from relations.
  4. Creates join stores for many-many relations with composite keys [from, to] and indices on both columns.
  5. Creates the built-in kv store for key-value operations.

Auto-Version Bumping

If the database already exists but is missing stores (for example, after adding new resources to the schema), the adapter automatically:

  1. Detects missing stores after opening.
  2. Closes the database.
  3. Reopens with a bumped version number to trigger onupgradeneeded.
  4. Creates the missing stores.

Version Limit

IndexedDB versions increment over time as the schema evolves. When the version exceeds MAX_IDB_VERSION (1000), the adapter recreates the database from scratch by deleting and reopening it. This prevents version number overflow in long-lived applications.

Changelog Deduplication

The changelog store has a unique compound index on [clientId, mutationId]. When changelogAppend is called with a duplicate (clientId, mutationId) pair, the existing entry is returned instead of creating a duplicate.

Deterministic Ordering

All list operations return records ordered by id ascending, ensuring deterministic output regardless of insertion order.

Internal Stores

The adapter maintains several internal IndexedDB object stores:

StoreKeyPurpose
<resource>idPer-resource record storage
<from>.<relation>.<to>[from, to]Many-many join rows
kvidBuilt-in key-value store
meta[type, key]Cursors and hydration state
changelogseq (auto-increment)Offline mutation log

Health Check

The adapter provides a healthCheck() method that verifies:

  • The database is accessible.
  • All expected object stores exist.
  • The cursor store is readable.
  • No stuck migrations are detected.
const { ok, issues } = await storage.healthCheck();
if (!ok) {
  console.error("Storage issues:", issues);
}

Lifecycle

// Close the database connection
await storage.close();

// Delete all data from all stores
await storage.clearAll();