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:
- Creates an object store for each resource (keyed by
id). - Creates indices from the resource schema (e.g., fields listed in
indices). - Creates foreign key indices inferred from relations.
- Creates join stores for many-many relations with composite keys
[from, to]and indices on both columns. - Creates the built-in
kvstore 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:
- Detects missing stores after opening.
- Closes the database.
- Reopens with a bumped version number to trigger
onupgradeneeded. - 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:
| Store | Key | Purpose |
|---|---|---|
<resource> | id | Per-resource record storage |
<from>.<relation>.<to> | [from, to] | Many-many join rows |
kv | id | Built-in key-value store |
meta | [type, key] | Cursors and hydration state |
changelog | seq (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();