DataFn
Storage

Storage Overview

Persistent offline storage adapters.

DataFn uses a pluggable storage adapter system for client-side persistence. The storage adapter provides the local data layer that enables offline-first operation, including record storage, join rows for many-many relations, sync cursors, hydration state, and the mutation changelog.

DatafnStorageAdapter Interface

Every storage adapter implements the DatafnStorageAdapter interface:

interface DatafnStorageAdapter {
  // Records
  getRecord(resource: string, id: string): Promise<Record<string, unknown> | null>;
  listRecords(resource: string): Promise<Record<string, unknown>[]>;
  upsertRecord(resource: string, record: Record<string, unknown>): Promise<void>;
  deleteRecord(resource: string, id: string): Promise<void>;
  mergeRecord(
    resource: string,
    id: string,
    partial: Record<string, unknown>,
  ): Promise<Record<string, unknown>>;

  // Join rows (many-many relations)
  listJoinRows(relationKey: string): Promise<Array<Record<string, unknown>>>;
  getJoinRows(relationKey: string, fromId: string): Promise<Array<Record<string, unknown>>>;
  getJoinRowsInverse(relationKey: string, toId: string): Promise<Array<Record<string, unknown>>>;
  upsertJoinRow(relationKey: string, row: Record<string, unknown>): Promise<void>;
  setJoinRows(relationKey: string, rows: Array<Record<string, unknown>>): Promise<void>;
  deleteJoinRow(relationKey: string, from: string, to: string): Promise<void>;

  // Convenience query
  findRecords(resource: string, field: string, value: unknown): Promise<Record<string, unknown>[]>;

  // Sync state
  getCursor(resource: string): Promise<string | null>;
  setCursor(resource: string, cursor: string | null): Promise<void>;
  getHydrationState(resource: string): Promise<DatafnHydrationState>;
  setHydrationState(resource: string, state: DatafnHydrationState): Promise<void>;

  // Changelog
  changelogAppend(entry: Omit<DatafnChangelogEntry, "seq">): Promise<DatafnChangelogEntry>;
  changelogList(options?: { limit?: number }): Promise<DatafnChangelogEntry[]>;
  changelogAck(options: { throughSeq: number }): Promise<void>;

  // Counts (for reconcile)
  countRecords(resource: string): Promise<number>;
  countJoinRows(relationKey: string): Promise<number>;

  // Lifecycle
  close(): Promise<void>;
  clearAll(): Promise<void>;
  healthCheck(): Promise<{ ok: boolean; issues: string[] }>;
}

Key Concepts

Merge Record

The mergeRecord method performs an atomic read-modify-write operation. It reads the existing record, shallow-merges the provided fields, and writes the result back in a single transaction. For object-type fields, it performs one-level-deep merge.

// Existing record: { id: "t1", title: "Buy milk", meta: { color: "red", size: 3 } }
await storage.mergeRecord("todos", "t1", { meta: { color: "blue" } });
// Result: { id: "t1", title: "Buy milk", meta: { color: "blue", size: 3 } }

Join Rows

Many-many relations are stored in join stores with composite keys (from, to). The relation key follows the pattern fromResource.relationName.toResource:

await storage.upsertJoinRow("todos.tags.tags", { from: "t1", to: "tag1" });
const tags = await storage.getJoinRows("todos.tags.tags", "t1");
const todos = await storage.getJoinRowsInverse("todos.tags.tags", "tag1");

Changelog

The changelog is a write-ahead log for offline mutations. It deduplicates entries by (clientId, mutationId) pair to prevent duplicate writes from retries.

Hydration State

Each resource tracks its hydration state independently: "notStarted", "hydrating", "ready", or "failed". State transitions are validated to prevent invalid sequences.

Storage Factory

For multi-user or multi-tenant applications, use a DatafnStorageFactory to create isolated storage instances per auth context:

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

const storageFactory: DatafnStorageFactory = (context) => {
  return IndexedDbStorageAdapter.createForUser(
    "my-app",
    context.userId,
    context.tenantId,
  );
};

Available Adapters

AdapterPersistenceUse Case
IndexedDBBrowser IndexedDBProduction browser apps
MemoryIn-memory (ephemeral)Testing, SSR, non-browser environments
CustomYour implementationSpecialized storage needs