UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

282 lines (214 loc) 11.1 kB
--- name: db-core/persistence description: > SQLite-backed persistence for TanStack DB collections. persistedCollectionOptions wraps any adapter (Electric, Query, PowerSync, or local-only) with durable local storage. Platform adapters: browser (WA-SQLite OPFS), React Native (op-sqlite), Expo (expo-sqlite), Electron (IPC), Node (better-sqlite3), Capacitor, Tauri, Cloudflare Durable Objects. Multi-tab/multi-process coordination via BrowserCollectionCoordinator / ElectronCollectionCoordinator / SingleProcessCoordinator. schemaVersion for migration resets. Local-only mode for offline-first without a server. Applied transaction log pruning and safe full-reload recovery. type: sub-skill library: db library_version: '0.6.17' sources: - 'TanStack/db:packages/db-sqlite-persistence-core/src/persisted.ts' - 'TanStack/db:packages/browser-db-sqlite-persistence/src/index.ts' - 'TanStack/db:packages/react-native-db-sqlite-persistence/src/index.ts' - 'TanStack/db:packages/expo-db-sqlite-persistence/src/index.ts' - 'TanStack/db:packages/electron-db-sqlite-persistence/src/index.ts' - 'TanStack/db:packages/node-db-sqlite-persistence/src/index.ts' - 'TanStack/db:examples/react/offline-transactions/src/db/persisted-todos.ts' - 'TanStack/db:examples/react-native/shopping-list/src/db/collections.ts' --- This skill builds on db-core and db-core/collection-setup. Read those first. # SQLite Persistence TanStack DB persistence adds a durable SQLite-backed layer to any collection. Data survives page reloads, app restarts, and offline periods. The server remains authoritative for synced collections -- persistence provides a local cache that hydrates instantly. ## Choosing a Platform Package | Platform | Package | Create function | | -------------- | ------------------------------------------------------------ | -------------------------------------------- | | Browser (OPFS) | `@tanstack/browser-db-sqlite-persistence` | `createBrowserWASQLitePersistence` | | React Native | `@tanstack/react-native-db-sqlite-persistence` | `createReactNativeSQLitePersistence` | | Expo | `@tanstack/expo-db-sqlite-persistence` | `createExpoSQLitePersistence` | | Electron | `@tanstack/electron-db-sqlite-persistence` | `createElectronSQLitePersistence` (renderer) | | Node.js | `@tanstack/node-db-sqlite-persistence` | `createNodeSQLitePersistence` | | Capacitor | `@tanstack/capacitor-db-sqlite-persistence` | `createCapacitorSQLitePersistence` | | Tauri | `@tanstack/tauri-db-sqlite-persistence` | `createTauriSQLitePersistence` | | Cloudflare DO | `@tanstack/cloudflare-durable-objects-db-sqlite-persistence` | `createCloudflareDOSQLitePersistence` | All platform packages re-export `persistedCollectionOptions` from the core. ## Local-Only Persistence (No Server) For purely local data with no sync backend: ```ts import { createCollection } from '@tanstack/react-db' import { createBrowserWASQLitePersistence, openBrowserWASQLiteOPFSDatabase, persistedCollectionOptions, } from '@tanstack/browser-db-sqlite-persistence' const database = await openBrowserWASQLiteOPFSDatabase({ databaseName: 'my-app.sqlite', }) const persistence = createBrowserWASQLitePersistence({ database, }) const draftsCollection = createCollection( persistedCollectionOptions<Draft, string>({ id: 'drafts', getKey: (d) => d.id, persistence, schemaVersion: 1, }), ) ``` Local-only collections provide `collection.utils.acceptMutations()` for applying mutations directly. ## Synced Persistence (Wrapping an Adapter) Spread an existing adapter's options into `persistedCollectionOptions` to add persistence on top of sync: ```ts import { createCollection } from '@tanstack/react-db' import { electricCollectionOptions } from '@tanstack/electric-db-collection' import { createReactNativeSQLitePersistence, persistedCollectionOptions, } from '@tanstack/react-native-db-sqlite-persistence' const persistence = createReactNativeSQLitePersistence({ database }) const todosCollection = createCollection( persistedCollectionOptions({ ...electricCollectionOptions({ id: 'todos', shapeOptions: { url: '/api/electric/todos' }, getKey: (item) => item.id, }), persistence, schemaVersion: 1, }), ) ``` This works with any adapter: `electricCollectionOptions`, `queryCollectionOptions`, `powerSyncCollectionOptions`, etc. The `persistedCollectionOptions` wrapper intercepts the sync layer to persist data as it flows through. ## Multi-Tab / Multi-Process Coordination Coordinators handle leader election and cross-instance communication so only one tab/process owns the database writer. | Platform | Coordinator | Mechanism | | ------------------------------------- | ------------------------------- | ---------------------------- | | Browser | `BrowserCollectionCoordinator` | BroadcastChannel + Web Locks | | Electron | `ElectronCollectionCoordinator` | BroadcastChannel + Web Locks | | Single-process (RN, Expo, Node, etc.) | `SingleProcessCoordinator` | No-op (always leader) | Browser persistence uses single-process semantics by default. That is correct when the app runs in one tab at a time or each tab has its own database. Pass a `BrowserCollectionCoordinator` only when multiple tabs share one OPFS database. Browser example: ```ts import { BrowserCollectionCoordinator } from '@tanstack/browser-db-sqlite-persistence' const coordinator = new BrowserCollectionCoordinator({ dbName: 'my-app', }) // Pass to persistence const persistence = createBrowserWASQLitePersistence({ database, coordinator }) // Cleanup on shutdown coordinator.dispose() ``` Electron requires setup in both processes: ```ts // Main process import { exposeElectronSQLitePersistence } from '@tanstack/electron-db-sqlite-persistence' import { app, ipcMain } from 'electron' const disposeIpc = exposeElectronSQLitePersistence({ persistence, ipcMain }) app.on('before-quit', () => { disposeIpc() }) // Renderer process import { createElectronSQLitePersistence, ElectronCollectionCoordinator, } from '@tanstack/electron-db-sqlite-persistence' const coordinator = new ElectronCollectionCoordinator({ dbName: 'my-app' }) const persistence = createElectronSQLitePersistence({ ipcRenderer: window.electron.ipcRenderer, coordinator, }) ``` Electron persistence calls cross the renderer/main boundary through IPC. The `ElectronCollectionCoordinator` separately coordinates renderer instances with `BroadcastChannel` and Web Locks. ## Schema Versioning `schemaVersion` tracks the shape of persisted data. When the stored version doesn't match the code, the collection resets (drops and reloads from server for synced collections, or throws for local-only). ```ts persistedCollectionOptions({ // ... schemaVersion: 2, // bump when you change the data shape }) ``` There is no custom migration function -- a version mismatch triggers a full reset. For synced collections this is safe because the server re-supplies the data. ## Applied Transaction Log Pruning The SQLite `applied_tx` log is a replay cache, not permanent history. Browser, Capacitor, Cloudflare Durable Objects, Expo, Node, React Native, and Tauri wrappers prune it inside write transactions by default, per collection: - `appliedTxPruneMaxRows: 1_000` - `appliedTxPruneMaxAgeSeconds: 86_400` (24 hours) Set either option to `0` to disable that limit, or raise it to retain a longer replay window: ```ts const persistence = createNodeSQLitePersistence({ database, appliedTxPruneMaxRows: 5_000, appliedTxPruneMaxAgeSeconds: 0, }) ``` If a follower asks to recover from a point older than the retained log, it falls back to a full reload. Pruning does not itself shrink the SQLite file; use SQLite vacuum settings or separate maintenance when disk reclamation matters. The defaults are exported as `DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS` and `DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS`. Raw `createSQLiteCorePersistenceAdapter` calls do not inject these defaults. Electron uses whichever persistence adapter the main process supplies. ## Key Options | Option | Type | Description | | --------------- | -------------------------------- | -------------------------------------------------------- | | `persistence` | `PersistedCollectionPersistence` | Platform adapter + coordinator | | `schemaVersion` | `number` | Data version (default 1). Bump on schema changes | | `id` | `string` | Required for local-only. Collection identifier in SQLite | ## Common Mistakes ### CRITICAL Using local-only persistence without an `id` Wrong: ```ts persistedCollectionOptions({ getKey: (d) => d.id, persistence, // missing id — generates random UUID each session, data won't persist across reloads }) ``` Correct: ```ts persistedCollectionOptions({ id: 'drafts', getKey: (d) => d.id, persistence, }) ``` Without an explicit `id`, the code generates a random UUID each session, so persisted data is silently abandoned on every reload. Local-only persisted collections must always provide an `id`. Synced collections derive it from the adapter config. ### HIGH Sharing one browser database across tabs without a coordinator Wrong: ```ts const persistence = createBrowserWASQLitePersistence({ database }) // Unsafe if multiple tabs share this database ``` Correct: ```ts const coordinator = new BrowserCollectionCoordinator({ dbName: 'my-app' }) const persistence = createBrowserWASQLitePersistence({ database, coordinator }) ``` Without a coordinator, multiple browser tabs that share one OPFS database can write concurrently. Use `BrowserCollectionCoordinator` for that case. Do not add it to a single-tab app merely because the runtime is a browser. ### HIGH Not bumping schemaVersion after changing data shape If you add, remove, or rename fields in your collection type but keep the same `schemaVersion`, the persisted SQLite data will have the old shape. For synced collections, bump the version to trigger a reset and re-sync. ### MEDIUM Not disposing the coordinator on cleanup ```ts // On app shutdown or hot module reload coordinator.dispose() await database.close?.() ``` Failing to dispose leaks BroadcastChannel subscriptions and Web Lock handles. See also: db-core/collection-setup/SKILL.md — for adapter selection and collection configuration. See also: offline/SKILL.md — for offline transaction queueing (complements persistence).