UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

242 lines (183 loc) 9.54 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. type: sub-skill library: db library_version: '0.6.0' 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 { BrowserCollectionCoordinator, createBrowserWASQLitePersistence, openBrowserWASQLiteOPFSDatabase, persistedCollectionOptions, } from '@tanstack/browser-db-sqlite-persistence' const database = await openBrowserWASQLiteOPFSDatabase({ databaseName: 'my-app.sqlite', }) const coordinator = new BrowserCollectionCoordinator({ dbName: 'my-app', }) const persistence = createBrowserWASQLitePersistence({ database, coordinator, }) 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` | IPC (main holds DB, renderer accesses via RPC) | | Single-process (RN, Expo, Node, etc.) | `SingleProcessCoordinator` | No-op (always leader) | 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' exposeElectronSQLitePersistence({ persistence, ipcMain }) // 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, }) ``` ## 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. ## 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 Forgetting the coordinator in multi-tab apps Wrong: ```ts const persistence = createBrowserWASQLitePersistence({ database }) // No coordinator — concurrent tabs corrupt the database ``` Correct: ```ts const coordinator = new BrowserCollectionCoordinator({ dbName: 'my-app' }) const persistence = createBrowserWASQLitePersistence({ database, coordinator }) ``` Without a coordinator, multiple browser tabs write to SQLite concurrently, causing data corruption. Always use `BrowserCollectionCoordinator` in browser environments. ### 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).