@tanstack/db
Version:
A reactive client store for building super fast apps on sync
242 lines (183 loc) • 9.54 kB
Markdown
---
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) | `/browser-db-sqlite-persistence` | `createBrowserWASQLitePersistence` |
| React Native | `/react-native-db-sqlite-persistence` | `createReactNativeSQLitePersistence` |
| Expo | `/expo-db-sqlite-persistence` | `createExpoSQLitePersistence` |
| Electron | `/electron-db-sqlite-persistence` | `createElectronSQLitePersistence` (renderer) |
| Node.js | `/node-db-sqlite-persistence` | `createNodeSQLitePersistence` |
| Capacitor | `/capacitor-db-sqlite-persistence` | `createCapacitorSQLitePersistence` |
| Tauri | `/tauri-db-sqlite-persistence` | `createTauriSQLitePersistence` |
| Cloudflare DO | `/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 '/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 '/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 '/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).