875 lines
35 KiB
Markdown
875 lines
35 KiB
Markdown
---
|
|
name: powersync-js
|
|
description: PowerSync JavaScript/TypeScript SDK — schema, backend connector, queries, transactions, sync status, and debugging
|
|
metadata:
|
|
tags: javascript, typescript, web, sqlite, offline-first
|
|
---
|
|
|
|
> **Load this when** working on any JavaScript or TypeScript project with PowerSync. This is the foundation file — always load it first, then load the applicable framework-specific file alongside it.
|
|
|
|
# PowerSync JavaScript/TypeScript SDK
|
|
|
|
Core patterns and guidance shared across all PowerSync JavaScript/TypeScript targets — schema design, the backend connector, database initialization, transactions, imperative queries, sync status, and debugging. For ORM integration see `powersync-js-orm.md`; for raw tables and sync internals see `powersync-js-raw-tables.md`.
|
|
|
|
## Table of Contents
|
|
|
|
- [Package Coverage](#package-coverage)
|
|
- [Quick Setup](#quick-setup) (Install, Schema, Backend Connector, uploadData, Initialize)
|
|
- [Query Patterns](#query-patterns) (useQuery, CompilableQuery, Imperative, Watch)
|
|
- [Writes & Transactions](#writes--transactions)
|
|
- [Sync Status, Priorities & Sync Streams](#sync-status-priorities--sync-streams)
|
|
- [Debugging](#debugging)
|
|
- [Common Pitfalls](#common-pitfalls)
|
|
|
|
**TypeScript-only exports:** `PowerSyncBackendConnector`, `PowerSyncCredentials`, and `AbstractPowerSyncDatabase` are **TypeScript interfaces only** — they exist at compile time for type checking but have no runtime presence. This means:
|
|
- Always use `import type` for these (e.g. `import type { PowerSyncBackendConnector } from '@powersync/web'`)
|
|
- Runtime checks like `require('@powersync/web').PowerSyncBackendConnector` will return `undefined` — this is expected, not a bug
|
|
- Only `UpdateType` is a runtime value (enum) and uses a regular import
|
|
- Bundlers like Vite will error if you import these without `type` since they try to resolve them as values
|
|
|
|
| Resource | Description |
|
|
|----------|-------------|
|
|
| [JS/TS Reference](https://docs.powersync.com/client-sdks/reference/javascript-web.md) | Full SDK documentation for Web, consult for details beyond the inline examples. |
|
|
| [Web SDK API Reference](https://powersync-ja.github.io/powersync-js/web-sdk) | Full API reference for `@powersync/web`, consult only when the inline examples don't cover your case. |
|
|
| [React Native Reference](https://docs.powersync.com/client-sdks/reference/react-native.md) | Full SDK documentation for React Native, consult for details beyond the inline examples. |
|
|
| [React Native SDK API Reference](https://powersync-ja.github.io/powersync-js/react-native-sdk) | Full API reference for `@powersync/react-native`, consult only when the inline examples don't cover your case. |
|
|
| [Capacitor Reference](https://docs.powersync.com/client-sdks/reference/capacitor.md) | Full SDK documentation for Capacitor, consult for details beyond the inline examples. |
|
|
| [Capacitor SDK API Reference](https://powersync-ja.github.io/powersync-js/capacitor-sdk) | Full API reference for `@powersync/capacitor`, consult only when the inline examples don't cover your case. |
|
|
| [Node.js Reference](https://docs.powersync.com/client-sdks/reference/node.md) | Full SDK documentation for Node.js, consult for details beyond the inline examples. |
|
|
| [Node.js SDK API Reference](https://powersync-ja.github.io/powersync-js/node-sdk) | Full API reference for `@powersync/node`, consult only when the inline examples don't cover your case. |
|
|
| [Supported Platforms - JS SDK](https://docs.powersync.com/resources/supported-platform.md#javascript-web-sdk) | Supported platforms and features, consult for compatibility details. |
|
|
|
|
Framework-specific files (load alongside this file):
|
|
|
|
| File | Use when... |
|
|
|------|-------------|
|
|
| `references/sdks/powersync-js-react.md` | React web app or Next.js |
|
|
| `references/sdks/powersync-js-react-native.md` | React Native, Expo, or Expo Go |
|
|
| `references/sdks/powersync-js-vue.md` | Vue or Nuxt |
|
|
| `references/sdks/powersync-js-node.md` | Node.js CLI/server or Electron |
|
|
| `references/sdks/powersync-js-tanstack.md` | TanStack Query or TanStack DB (any framework) |
|
|
|
|
## Package Coverage
|
|
|
|
| Need | Package |
|
|
|------|---------|
|
|
| Web browser | `@powersync/web` |
|
|
| React Native | `@powersync/react-native` |
|
|
| Node.js/CLI | `@powersync/node` |
|
|
| Capacitor | `@powersync/capacitor` |
|
|
| React hooks | `@powersync/react` |
|
|
| Vue composables | `@powersync/vue` |
|
|
| Nuxt module | `@powersync/nuxt` |
|
|
| TanStack Query (React) | `@powersync/tanstack-react-query` |
|
|
| TanStack DB (multi-framework) | `@tanstack/powersync-db-collection` |
|
|
| ORM | `@powersync/drizzle-driver` or `@powersync/kysely-driver` |
|
|
|
|
## Quick Setup
|
|
|
|
### 1. Install
|
|
|
|
```bash
|
|
# Web
|
|
npm install @powersync/web@latest
|
|
npm install @journeyapps/wa-sqlite@latest # Needed (peer-dependency)
|
|
|
|
# React Native
|
|
npm install @powersync/react-native@latest
|
|
npm install @powersync/powersync-op-sqlite@latest # Needed (peer-dependency)
|
|
|
|
# Node.js
|
|
npm install @powersync/node@latest
|
|
npm install better-sqlite3 # Needed (peer-dependency)
|
|
|
|
# React integration
|
|
npm install @powersync/react@latest
|
|
|
|
# Vue
|
|
npm install @powersync/vue@latest
|
|
|
|
# Nuxt (includes @powersync/vue — npm v7+ installs peers automatically)
|
|
npm install @powersync/nuxt@latest
|
|
|
|
# TanStack Query (React)
|
|
npm install @powersync/tanstack-react-query@latest
|
|
|
|
# TanStack DB
|
|
npm install @tanstack/powersync-db-collection@latest
|
|
```
|
|
|
|
Always install packages using `@latest` as shown above — PowerSync releases frequently and older cached versions can be missing critical fixes. Do not write version strings into `package.json` manually.
|
|
|
|
See the framework-specific files for full setup instructions per target.
|
|
|
|
### 2. Define Schema
|
|
|
|
```ts
|
|
import { column, Schema, Table } from '@powersync/web'; // or @powersync/react-native / @powersync/common
|
|
|
|
const todos = new Table(
|
|
{
|
|
// Do NOT define 'id' — PowerSync creates it automatically as TEXT PRIMARY KEY
|
|
list_id: column.text,
|
|
created_at: column.text, // Store dates as ISO strings — no date type
|
|
description: column.text,
|
|
completed: column.integer, // Store booleans as 0/1 — no boolean type
|
|
},
|
|
{
|
|
indexes: { list: ['list_id'] }, // Optional SQLite index
|
|
}
|
|
);
|
|
|
|
export const AppSchema = new Schema({ todos, lists });
|
|
export type Database = (typeof AppSchema)['types'];
|
|
export type Todo = Database['todos']; // Auto-generated row type
|
|
```
|
|
|
|
Column types: only `column.text`, `column.integer`, `column.real`. No boolean, no date, no JSON native type — store those as text/integer.
|
|
|
|
No migrations — schema changes apply automatically on next open. Removed columns become inaccessible (data still in DB). New columns start null. Renaming = adding new + removing old (data loss). See [Define the Client-Side Schema](https://docs.powersync.com/client-sdks/reference/javascript-web.md#1-define-the-client-side-schema) for more information.
|
|
|
|
### Special Table Types
|
|
|
|
See [Local-Only Tables](https://docs.powersync.com/usage/use-case-examples/local-only-tables.md) and [Insert-Only Tables](https://docs.powersync.com/usage/use-case-examples/insert-only-tables.md) for more information.
|
|
|
|
```ts
|
|
// Local-only — not synced from server, not uploaded, persists across restarts
|
|
const drafts = new Table({ title: column.text }, { localOnly: true });
|
|
|
|
// Insert-only — writes are uploaded but server never sends deletes
|
|
const logs = new Table({ message: column.text }, { insertOnly: true });
|
|
|
|
// Track previous values — available as op.previousValues in uploadData
|
|
const todos = new Table(
|
|
{ description: column.text, completed: column.integer },
|
|
{
|
|
trackPreviousValues: true,
|
|
// or: trackPreviousValues: { columns: ['completed'] }
|
|
// or: trackPreviousValues: { columns: ['completed'], onlyWhenChanged: true }
|
|
}
|
|
);
|
|
|
|
// Track metadata attached to individual writes
|
|
const tasks = new Table(
|
|
{ title: column.text },
|
|
{ trackMetadata: true } // Adds _metadata column, available as op.metadata in uploadData
|
|
);
|
|
```
|
|
|
|
### 3. Create Backend Connector
|
|
|
|
See [Integrate with your Backend](https://docs.powersync.com/client-sdks/reference/javascript-web.md#3-integrate-with-your-backend) and [Client-Side Integration](https://docs.powersync.com/configuration/app-backend/client-side-integration.md) for more information.
|
|
|
|
```ts
|
|
import type { PowerSyncBackendConnector, PowerSyncCredentials } from '@powersync/web'
|
|
async fetchCredentials(): Promise<PowerSyncCredentials> {
|
|
return {
|
|
endpoint: 'https://your-instance.powersync.journeyapps.com',
|
|
token: await getJwtFromAuthService(),
|
|
expiresAt: new Date(Date.now() + 3600_000), // optional hint for refresh timing
|
|
};
|
|
}
|
|
```
|
|
|
|
`fetchCredentials` is called automatically every few minutes when the sync stream reconnects. Must always return fresh credentials — do not return stale cached tokens.
|
|
|
|
`PowerSyncCredentials` interface: `{ endpoint: string; token: string; expiresAt?: Date }`. See [Authentication Setup](https://docs.powersync.com/configuration/auth/overview.md) for configuring JWT authentication.
|
|
|
|
### uploadData
|
|
|
|
Called automatically whenever local writes are pending. Must be synchronous with the actual backend write — do not queue operations for async processing elsewhere. If it throws, PowerSync backs off and retries automatically. See [Writing Client-Side Changes to your Backend](https://docs.powersync.com/usage/writing-client-side-changes-to-your-backend.md) for more information.
|
|
|
|
```ts
|
|
import { UpdateType } from '@powersync/web'
|
|
import type { AbstractPowerSyncDatabase, PowerSyncBackendConnector, PowerSyncCredentials } from '@powersync/web'
|
|
|
|
async uploadData(database: AbstractPowerSyncDatabase): Promise<void> {
|
|
const transaction = await database.getNextCrudTransaction();
|
|
if (!transaction) return;
|
|
|
|
try {
|
|
for (const op of transaction.crud) {
|
|
switch (op.op) {
|
|
case UpdateType.PUT:
|
|
await api.create(op.table, { id: op.id, ...op.opData });
|
|
break;
|
|
case UpdateType.PATCH:
|
|
await api.update(op.table, op.id, op.opData);
|
|
break;
|
|
case UpdateType.DELETE:
|
|
await api.delete(op.table, op.id);
|
|
break;
|
|
}
|
|
}
|
|
// MUST call complete() to advance the queue to the next transaction
|
|
await transaction.complete();
|
|
} catch (ex) {
|
|
// Throw to retry later — PowerSync will back off and retry
|
|
throw ex;
|
|
}
|
|
}
|
|
```
|
|
|
|
If `transaction.complete()` is never called, `getNextCrudTransaction()` returns the same transaction forever — the upload queue stalls permanently.
|
|
|
|
Note: When uploading to backends with native boolean columns (e.g. PostgreSQL via Supabase or MongoDB), op.opData will contain 0/1. Convert before writing.
|
|
|
|
#### HTTP Status Code Handling
|
|
|
|
- Return 2xx from backend even for validation errors — a 4xx blocks the upload queue permanently
|
|
- 5xx → PowerSync retries automatically with backoff
|
|
- Surface validation errors by writing them to a local-only table and showing in the UI — never let them block the queue
|
|
|
|
#### getCrudBatch vs getNextCrudTransaction
|
|
|
|
Two ways to consume the upload queue:
|
|
|
|
```ts
|
|
// getNextCrudTransaction — exactly one transaction's worth, all entries share transactionId
|
|
const tx = await db.getNextCrudTransaction();
|
|
if (tx) {
|
|
for (const op of tx.crud) { /* op.transactionId is the same for all */ }
|
|
await tx.complete();
|
|
}
|
|
|
|
// getCrudBatch — up to N entries, may span multiple transactions
|
|
const batch = await db.getCrudBatch(100);
|
|
if (batch) {
|
|
for (const op of batch.crud) { /* may have different transactionIds */ }
|
|
await batch.complete();
|
|
// batch.haveMore === true means there are more entries waiting
|
|
}
|
|
```
|
|
|
|
`getNextCrudTransaction` is used in most connector examples — simpler, guarantees atomicity per write transaction. `getCrudBatch` is useful when you want to batch across transaction boundaries for backend throughput.
|
|
|
|
#### CrudEntry Fields
|
|
|
|
```ts
|
|
interface CrudEntry {
|
|
clientId: number; // Auto-incrementing local ID
|
|
id: string; // Row ID
|
|
op: UpdateType; // PUT | PATCH | DELETE
|
|
opData?: Record<string, any>; // Changed columns — undefined for DELETE
|
|
previousValues?: Record<string, any>; // Previous values — requires trackPreviousValues on table
|
|
table: string; // Table name
|
|
transactionId?: number; // Groups ops from the same writeTransaction()
|
|
metadata?: string; // Custom metadata — requires trackMetadata on table
|
|
}
|
|
```
|
|
|
|
Op types (`UpdateType` enum):
|
|
- `PUT` — full insert or replace (new row, or complete overwrite)
|
|
- `PATCH` — partial update (`opData` contains only the changed columns)
|
|
- `DELETE` — deletion (`opData` is undefined)
|
|
|
|
`previousValues` is populated for PATCH and DELETE ops when the table has `trackPreviousValues: true`. Useful for implementing last-write-wins conflict resolution on the backend.
|
|
|
|
### 4. Initialize Database and Connect
|
|
|
|
See [Instantiate the PowerSync Database](https://docs.powersync.com/client-sdks/reference/javascript-web.md#2-instantiate-the-powersync-database) for more information.
|
|
|
|
```ts
|
|
// 1. Instantiate — schema applied at construction, no migrations
|
|
const db = new PowerSyncDatabase({ schema, database: { dbFilename: 'app.db' } });
|
|
|
|
// 2. Connect — starts sync stream and uploadData loop in background
|
|
db.connect(connector);
|
|
|
|
// 3. Optionally wait for first sync before rendering data
|
|
await db.waitForFirstSync();
|
|
```
|
|
|
|
`connect()` does not block — sync happens in the background. Do NOT `await connect()` thinking data is ready after it returns.
|
|
|
|
### 5. Provider / Plugin Setup
|
|
|
|
Framework-specific setup (React `PowerSyncContext.Provider`, Vue plugin, Nuxt plugin) is covered in the framework files. See `references/sdks/powersync-js-react.md`, `references/sdks/powersync-js-vue.md`, etc.
|
|
|
|
### Web-Specific Options
|
|
|
|
```ts
|
|
const db = new PowerSyncDatabase({
|
|
schema,
|
|
database: {
|
|
dbFilename: 'app.db',
|
|
debugMode: true // Logs all SQL to Chrome DevTools Performance timeline
|
|
},
|
|
flags: {
|
|
useWebWorker: true, // Default true — runs DB in a web worker
|
|
enableMultiTabs: true // Default true — shares sync worker across tabs
|
|
}
|
|
});
|
|
```
|
|
|
|
Multi-tab behavior: By default the web SDK uses a shared sync worker so all tabs share sync state. Only the most recently opened tab runs `fetchCredentials` and `uploadData`. Disable with `enableMultiTabs: false` if causing issues — but then only the oldest tab syncs.
|
|
|
|
#### VFS Options
|
|
|
|
| VFS Option | Description | Reference URL |
|
|
|---------------------------|---------------------|---------------------------------------------------------------------------------------------------------|
|
|
| IDBBatchAtomicVFS | Default | [Link](https://docs.powersync.com/client-sdks/reference/javascript-web.md#1-idbbatchatomicvfs-default) |
|
|
| OPFSCoopSyncVFS | Recommended | [Link](https://docs.powersync.com/client-sdks/reference/javascript-web.md#2-opfs-based-alternatives) |
|
|
|
|
```ts
|
|
// Recommended — more reliable across browsers including Safari
|
|
import { WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web'
|
|
|
|
const db = new PowerSyncDatabase({
|
|
schema,
|
|
database: new WASQLiteOpenFactory({
|
|
dbFilename: 'app.db',
|
|
vfs: WASQLiteVFS.OPFSCoopSyncVFS, // default: IDBBatchAtomicVFS
|
|
}),
|
|
})
|
|
```
|
|
|
|
Safari: Requires `OPFSCoopSyncVFS` for stable multi-tab, or set `useWebWorker: false`. See [Web SDK Reference](https://docs.powersync.com/client-sdks/reference/javascript-web.md) for full configuration options.
|
|
|
|
## Query Patterns
|
|
|
|
See [Using PowerSync: CRUD functions](https://docs.powersync.com/client-sdks/reference/javascript-web.md#using-powersync-crud-functions) for the full API reference.
|
|
|
|
### useQuery
|
|
|
|
```ts
|
|
useQuery<RowType>(
|
|
query: string | CompilableQuery<RowType>,
|
|
parameters?: any[],
|
|
options?: {
|
|
rowComparator?: { keyBy, compareBy },
|
|
reportFetching?: boolean,
|
|
throttleMs?: number,
|
|
runQueryOnce?: boolean,
|
|
streams?: QuerySyncStreamOptions[],
|
|
}
|
|
): { data, isLoading, isFetching, error }
|
|
```
|
|
|
|
Parameters are compared by `JSON.stringify` value, not by reference — so `[userId]` across renders are considered equal even as different array instances.
|
|
|
|
Pitfall: Avoid passing objects that serialize differently between renders (e.g. objects with changing key order).
|
|
|
|
#### rowComparator — Differential Mode
|
|
|
|
Without `rowComparator`: every change to any watched table re-runs the query and returns a new array reference — all children re-render regardless of whether their row changed.
|
|
|
|
With `rowComparator`: uses differential watch internally. Only rows that actually changed get new object references. Unchanged rows keep the same object reference, so `React.memo` can skip re-rendering them.
|
|
|
|
```tsx
|
|
const { data: lists } = useQuery('SELECT * FROM lists', [], {
|
|
rowComparator: {
|
|
keyBy: (row) => row.id,
|
|
compareBy: (row) => JSON.stringify(row)
|
|
}
|
|
});
|
|
|
|
const ListItem = React.memo(({ list }) => <Text>{list.name}</Text>);
|
|
// Only re-renders when list.name or other fields actually change
|
|
```
|
|
|
|
#### runQueryOnce
|
|
|
|
Runs the query once after sync completes — no live watch. Useful for aggregations or reports.
|
|
|
|
```ts
|
|
const { data } = useQuery('SELECT COUNT(*) as total FROM lists', [], { runQueryOnce: true });
|
|
```
|
|
|
|
#### streams option
|
|
|
|
Gates the query on specific named sync streams having synced before executing.
|
|
|
|
```ts
|
|
const { data } = useQuery('SELECT * FROM lists', [], {
|
|
streams: [{ name: 'lists', parameters: { userId }, waitForStream: true }]
|
|
});
|
|
// Returns isLoading: true until the 'lists' stream has synced
|
|
```
|
|
|
|
### Compiling Queries (CompilableQuery)
|
|
|
|
Both hooks accept a `CompilableQuery` object in addition to a plain SQL string. This is useful when using [Drizzle](https://docs.powersync.com/client-sdks/orms/javascript-web/drizzle.md) or [Kysely](https://docs.powersync.com/client-sdks/orms/javascript-web/kysely.md) integrations:
|
|
|
|
```ts
|
|
// With Drizzle
|
|
const query = db.select().from(lists).where(eq(lists.ownerId, userId));
|
|
const { data } = useQuery(query);
|
|
|
|
// With Kysely
|
|
const query = db.selectFrom('lists').selectAll().where('owner_id', '=', userId);
|
|
const { data } = useQuery(query);
|
|
```
|
|
|
|
### One-Time Queries (Imperative)
|
|
|
|
```ts
|
|
// Get all
|
|
const todos = await db.getAll('SELECT * FROM todos WHERE list_id = ?', [listId]);
|
|
|
|
// Get one (throws if not found)
|
|
const todo = await db.get('SELECT * FROM todos WHERE id = ?', [id]);
|
|
|
|
// Get optional (returns null if not found)
|
|
const todo = await db.getOptional('SELECT * FROM todos WHERE id = ?', [id]);
|
|
```
|
|
|
|
### Watch Queries (Imperative)
|
|
|
|
Outside of React, use the async generator API directly:
|
|
|
|
```ts
|
|
for await (const result of db.watchWithAsyncGenerator('SELECT * FROM lists')) {
|
|
console.log(result.rows._array);
|
|
}
|
|
|
|
// Or with a differential watch (only emits on actual changes)
|
|
const watchedQuery = db.customQuery({
|
|
compile: () => ({ sql: 'SELECT * FROM lists', parameters: [] }),
|
|
execute: () => db.getAll('SELECT * FROM lists')
|
|
}).watch({ reportFetching: true });
|
|
|
|
const dispose = watchedQuery.registerListener({
|
|
onData: (data) => console.log(data),
|
|
onError: (err) => console.error(err)
|
|
});
|
|
|
|
// Later:
|
|
dispose();
|
|
```
|
|
|
|
## Writes & Transactions
|
|
|
|
### Single Operation
|
|
|
|
```ts
|
|
await db.execute(
|
|
'INSERT INTO lists (id, created_at, name, owner_id) VALUES (uuid(), datetime(), ?, ?)',
|
|
['My List', userId]
|
|
);
|
|
```
|
|
|
|
### writeTransaction — Multiple Related Operations
|
|
|
|
Use when multiple operations must be atomic. Auto-commits on success, auto-rollbacks if an exception is thrown.
|
|
|
|
```ts
|
|
await db.writeTransaction(async (tx) => {
|
|
await tx.execute('DELETE FROM lists WHERE id = ?', [listId]);
|
|
await tx.execute('DELETE FROM todos WHERE list_id = ?', [listId]);
|
|
// No need to call commit() — it's automatic
|
|
});
|
|
```
|
|
|
|
When to use `writeTransaction`:
|
|
- Multiple operations that must succeed or fail together
|
|
- Cascading deletes, multi-table updates
|
|
- Any situation where partial completion would leave data inconsistent
|
|
|
|
When NOT to use:
|
|
- Single operations — `db.execute()` is simpler and faster
|
|
- Read-only queries — use `readTransaction` or `getAll`/`get`
|
|
|
|
### readTransaction
|
|
|
|
```ts
|
|
const result = await db.readTransaction(async (tx) => {
|
|
const lists = await tx.getAll('SELECT * FROM lists');
|
|
const count = await tx.get('SELECT COUNT(*) as n FROM todos');
|
|
return { lists, count };
|
|
});
|
|
```
|
|
|
|
### ID Generation
|
|
|
|
PowerSync auto-creates an `id TEXT` column on every table — do not declare it in the schema. Generate UUIDs client-side:
|
|
|
|
```ts
|
|
// SQLite uuid() function is available
|
|
await db.execute('INSERT INTO todos (id, description) VALUES (uuid(), ?)', ['Buy milk']);
|
|
|
|
// Or generate in JS
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
await db.execute('INSERT INTO todos (id, description) VALUES (?, ?)', [uuidv4(), 'Buy milk']);
|
|
```
|
|
|
|
### Lock Behavior
|
|
|
|
- Only ONE write transaction executes at a time (global write mutex)
|
|
- Default lock timeout: 120 seconds — increase only if operations genuinely take longer
|
|
- `writeTransaction()` takes the lock for the entire callback duration
|
|
- Multiple rapid writes are more efficient when batched inside a single `writeTransaction`
|
|
|
|
## Sync Status, Priorities & Sync Streams
|
|
|
|
### useStatus Hook
|
|
|
|
```ts
|
|
const status = useStatus();
|
|
// {
|
|
// connected: boolean,
|
|
// connecting: boolean,
|
|
// lastSyncedAt: Date | null,
|
|
// hasSynced: boolean, // true after first full sync, persists across restarts
|
|
// isSyncing: boolean,
|
|
// downloadProgress: DownloadProgress | null,
|
|
// dataFlowStatus: {
|
|
// uploading: boolean,
|
|
// downloading: boolean,
|
|
// uploadError: Error | undefined, // set on upload failure, cleared on next success
|
|
// downloadError: Error | undefined, // set on download/connect failure, cleared on next success
|
|
// downloadProgress: ...
|
|
// }
|
|
// }
|
|
```
|
|
|
|
#### uploadError and downloadError
|
|
|
|
`status.dataFlowStatus.uploadError` and `status.dataFlowStatus.downloadError` are the primary way to surface sync failures to users or logging systems.
|
|
|
|
- `uploadError` — set when an exception occurs during the CRUD upload loop. Cleared automatically on the next successful upload.
|
|
- `downloadError` — set when an exception occurs during the streaming sync (including connection failures). Cleared on the next successful data download or checkpoint completion.
|
|
|
|
```tsx
|
|
const status = useStatus();
|
|
|
|
if (status.dataFlowStatus?.uploadError) {
|
|
return <Banner>Failed to save changes: {status.dataFlowStatus.uploadError.message}</Banner>;
|
|
}
|
|
if (status.dataFlowStatus?.downloadError) {
|
|
return <Banner>Sync error: {status.dataFlowStatus.downloadError.message}</Banner>;
|
|
}
|
|
```
|
|
|
|
Register a status listener imperatively (useful for logging, not just UI):
|
|
|
|
```ts
|
|
db.registerListener({
|
|
statusChanged: (status) => {
|
|
if (status.dataFlowStatus?.downloadError) {
|
|
logger.error('PowerSync download failed', {
|
|
error: status.dataFlowStatus.downloadError,
|
|
lastSyncedAt: status.lastSyncedAt,
|
|
connected: status.connected,
|
|
});
|
|
}
|
|
if (status.dataFlowStatus?.uploadError) {
|
|
logger.error('PowerSync upload failed', {
|
|
error: status.dataFlowStatus.uploadError,
|
|
lastSyncedAt: status.lastSyncedAt,
|
|
connected: status.connected,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
### waitForFirstSync
|
|
|
|
`db.waitForFirstSync()` resolves when all data has been downloaded at least once. After that, `db.currentStatus.hasSynced` is `true` and persists across app restarts (stored in the local DB).
|
|
|
|
```ts
|
|
// Standard usage — gate app rendering behind first sync
|
|
db.connect(connector);
|
|
await db.waitForFirstSync();
|
|
// Now safe to render data-dependent screens
|
|
|
|
// With abort signal (e.g. timeout after 10s)
|
|
const controller = new AbortController();
|
|
setTimeout(() => controller.abort(), 10_000);
|
|
await db.waitForFirstSync(controller.signal);
|
|
```
|
|
|
|
### Sync Priorities
|
|
|
|
Streams (or buckets in legacy Sync Rules) can be assigned priorities (0-3). Lower numbers = higher priority. Higher-priority data syncs first, allowing partial data to appear before the full sync completes. See [Prioritized Sync](https://docs.powersync.com/usage/use-case-examples/prioritized-sync.md) for more information.
|
|
|
|
Priority 0 is special: it syncs regardless of pending uploads — use carefully as it can cause temporary inconsistencies.
|
|
|
|
Consistency caveat: Full PowerSync consistency guarantees (including deletes) only apply once ALL buckets at all priorities have synced. Higher-priority partial syncs may have stale deletes until lower-priority buckets complete.
|
|
|
|
#### waitForFirstSync with Priority
|
|
|
|
```ts
|
|
// Wait only for priority-1 buckets (faster — show UI sooner)
|
|
await db.waitForFirstSync({ priority: 1 });
|
|
|
|
// With abort signal + priority
|
|
const controller = new AbortController();
|
|
setTimeout(() => controller.abort(), 10_000);
|
|
await db.waitForFirstSync({ signal: controller.signal, priority: 1 });
|
|
```
|
|
|
|
#### Download Progress UI
|
|
|
|
```tsx
|
|
const status = useStatus();
|
|
const progress = status.downloadProgress;
|
|
|
|
// Overall progress
|
|
if (progress) {
|
|
return <ProgressBar value={progress.downloadedFraction} />;
|
|
// progress.downloadedOperations / progress.totalOperations also available
|
|
}
|
|
|
|
// Progress up to a specific priority only
|
|
const priorityProgress = progress?.untilPriority(1);
|
|
if (priorityProgress) {
|
|
return <ProgressBar value={priorityProgress.downloadedFraction} />;
|
|
}
|
|
```
|
|
|
|
#### Per-Priority Status
|
|
|
|
```ts
|
|
// Check if a specific priority has synced
|
|
const p1Status = db.currentStatus.statusForPriority(1);
|
|
// p1Status.hasSynced, p1Status.lastSyncedAt
|
|
|
|
// List all priority statuses seen
|
|
const entries = db.currentStatus.priorityStatusEntries();
|
|
// [{ priority: 1, hasSynced: true, lastSyncedAt: Date }, ...]
|
|
```
|
|
|
|
### Sync Streams
|
|
|
|
Sync Streams are the recommended way to define what data syncs to each client. They provide on-demand subscriptions with parameters and TTL-based expiry. See [sync-config.md](references/sync-config.md) for server-side configuration (YAML definitions, parameters, CTEs).
|
|
|
|
Requires the service to be configured with Sync Streams (edition 3 config). See [Sync Streams Overview](https://docs.powersync.com/sync/streams/overview.md) and [Client-Side Usage](https://docs.powersync.com/sync/streams/client-usage.md) for more information.
|
|
|
|
The `streams` option in `useQuery` (see below) and the imperative API work across all JS/TS frameworks. Framework-specific Sync Stream hooks are covered in the respective framework files where available — for example, `useSyncStream` and `useSuspenseSyncStream` in `references/sdks/powersync-js-react.md`.
|
|
|
|
#### Streams in useQuery
|
|
|
|
Gate a query on a specific stream having synced, without managing the subscription manually:
|
|
|
|
```ts
|
|
const { data: lists } = useQuery('SELECT * FROM lists', [], {
|
|
streams: [
|
|
{
|
|
name: 'lists',
|
|
parameters: { userId },
|
|
waitForStream: true, // hold isLoading: true until this stream syncs
|
|
priority: 1,
|
|
ttl: 3600,
|
|
}
|
|
]
|
|
});
|
|
```
|
|
|
|
#### Imperative API
|
|
|
|
```ts
|
|
// Subscribe directly
|
|
const subscription = await db.syncStream('lists', { userId }).subscribe({
|
|
priority: 1,
|
|
ttl: 3600
|
|
});
|
|
|
|
// Wait for this specific stream to sync
|
|
await subscription.waitForFirstSync();
|
|
|
|
// Check status
|
|
const streamStatus = db.currentStatus.forStream(subscription);
|
|
console.log(streamStatus.subscription.hasSynced);
|
|
|
|
// Unsubscribe when done
|
|
subscription.unsubscribe();
|
|
```
|
|
|
|
#### Stream Gotchas
|
|
|
|
- Parameters as identity: same stream name with different parameters = separate subscriptions
|
|
- Partial checkpoints: only the Rust sync client supports partial checkpoints (priority-level consistency)
|
|
- Default streams: server may configure streams as default — these subscribe automatically without a client call
|
|
- TTL eviction: after TTL expires with no active subscriber, the stream's data may be removed from the local DB
|
|
|
|
## ORM & Raw Tables
|
|
|
|
These advanced topics are in separate files — load only when needed:
|
|
|
|
| Topic | File | Load when… |
|
|
|-------|------|-----------|
|
|
| Drizzle / Kysely ORM | `references/sdks/powersync-js-orm.md` | Using Drizzle or Kysely for type-safe queries |
|
|
| Raw Tables | `references/raw-tables.md` | Need native SQLite tables (SDK-agnostic — JS, Dart, Kotlin, Swift, Rust) |
|
|
|
|
## JS Internals
|
|
|
|
> Only needed when debugging QueryStore eviction, investigating sync client implementations, or working with internal op types.
|
|
|
|
### Sync Client Implementations
|
|
|
|
The Rust-based sync client is now the only sync client. The legacy JavaScript client (`SyncClientImplementation.JAVASCRIPT` / `@LegacySyncImplementation` APIs) has been removed from `@powersync/common`, `@powersync/web`, and `@powersync/react-native` and is no longer an option. Do not downgrade the SDK to a version that still shipped the JS client — older versions can't read the Rust client's storage format.
|
|
|
|
### QueryStore
|
|
|
|
`useSuspenseQuery` uses a `QueryStore` (one per `PowerSyncDatabase`, stored in a `WeakMap`). Caches `WatchedQuery` instances keyed by `"${sql} -- ${JSON.stringify(params)} -- ${JSON.stringify(options)}"`. Evicted when listener count reaches 0. `useSuspenseQuery` and `useQuery` with the same SQL/params/options share the same underlying `WatchedQuery`.
|
|
|
|
### Op Types (Internal Sync vs CRUD)
|
|
|
|
Internal bucket ops (`OpTypeEnum`): `CLEAR=1`, `MOVE=2`, `PUT=3`, `REMOVE=4` — sync protocol only, not exposed to userland.
|
|
CRUD upload ops (`UpdateType`): `PUT`, `PATCH`, `DELETE` — what you see in `uploadData`. Don't confuse sync-level `REMOVE` with CRUD-level `DELETE`.
|
|
|
|
## Debugging
|
|
|
|
See [Debugging Overview](https://docs.powersync.com/debugging/tools-and-techniques.md) for the full list of tools and techniques.
|
|
|
|
### Sync Diagnostics Client
|
|
|
|
https://diagnostics-app.powersync.com
|
|
|
|
Connect this to a running PowerSync instance to inspect tables, rows, sync buckets, and run arbitrary SQL against the local database. This is the fastest way to isolate whether a problem is in the PowerSync service or in the client:
|
|
|
|
- If the Sync Diagnostics Client shows the correct data → the service is syncing correctly → the issue is in your client code (query, schema, rendering)
|
|
- If the Sync Diagnostics Client shows incorrect or missing data → the issue is in the PowerSync service configuration (sync rules, backend connector, permissions)
|
|
|
|
### Enable SDK Logging (Development)
|
|
|
|
```ts
|
|
import { createBaseLogger, LogLevel } from '@powersync/react'; // or @powersync/common
|
|
|
|
const logger = createBaseLogger();
|
|
logger.useDefaults(); // output to console
|
|
logger.setLevel(LogLevel.DEBUG); // DEBUG | INFO | WARN | ERROR | TRACE | OFF
|
|
```
|
|
|
|
### Production Logging
|
|
|
|
Enable PowerSync logging in production — it is extremely helpful for debugging sync issues reported by users. Use whatever logging provider your app already uses (Sentry, Datadog, Firebase Crashlytics, etc.).
|
|
|
|
The key pattern is: use `WARN` level in production (captures errors and warnings without noise), and pipe warnings/errors to your log aggregation service. Capture all levels as breadcrumbs so you have context leading up to an error.
|
|
|
|
Example using Sentry (substitute your own provider):
|
|
|
|
```ts
|
|
import { createBaseLogger, LogLevel } from '@powersync/react-native';
|
|
|
|
const logger = createBaseLogger();
|
|
logger.useDefaults();
|
|
logger.setLevel(LogLevel.WARN); // WARN and above in production
|
|
|
|
logger.setHandler((messages, context) => {
|
|
if (!context?.level) return;
|
|
|
|
const messageArray = Array.from(messages);
|
|
const mainMessage = String(messageArray[0] || '');
|
|
const extra = messageArray.slice(1).reduce((acc, curr) => ({ ...acc, ...curr }), {});
|
|
const level = context.level.name.toLowerCase();
|
|
|
|
// Capture everything as breadcrumbs for pre-error context
|
|
Sentry.addBreadcrumb({
|
|
message: mainMessage,
|
|
level: level as Sentry.SeverityLevel,
|
|
data: extra,
|
|
timestamp: Date.now()
|
|
});
|
|
|
|
// Only send warn/error to the logging service
|
|
if (level === 'warn' || level === 'error') {
|
|
Sentry.logger[level](mainMessage, extra);
|
|
}
|
|
});
|
|
```
|
|
|
|
Also register a status listener to capture `uploadError` and `downloadError` — these won't appear in the SDK logger automatically:
|
|
|
|
```ts
|
|
db.registerListener({
|
|
statusChanged: (status) => {
|
|
if (status.dataFlowStatus?.downloadError) {
|
|
logger.error('PowerSync download error', {
|
|
error: status.dataFlowStatus.downloadError,
|
|
lastSyncedAt: status.lastSyncedAt,
|
|
connected: status.connected,
|
|
sdkVersion: db.sdkVersion,
|
|
});
|
|
}
|
|
if (status.dataFlowStatus?.uploadError) {
|
|
logger.error('PowerSync upload error', {
|
|
error: status.dataFlowStatus.uploadError,
|
|
lastSyncedAt: status.lastSyncedAt,
|
|
connected: status.connected,
|
|
sdkVersion: db.sdkVersion,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
Context to include in logs: user/session ID, SDK version (`db.sdkVersion`), `lastSyncedAt`, `connected` status. Avoid logging sensitive row data.
|
|
|
|
### Web: SQL Logging to Chrome Performance Timeline
|
|
|
|
```ts
|
|
const db = new PowerSyncDatabase({
|
|
schema,
|
|
database: { dbFilename: 'app.db', debugMode: true }
|
|
});
|
|
// All SQL appears in Chrome DevTools → Performance tab timeline
|
|
```
|
|
|
|
### Check Sync Status Imperatively
|
|
|
|
```ts
|
|
console.log(db.currentStatus);
|
|
// { connected, connecting, lastSyncedAt, hasSynced, isSyncing, downloadProgress }
|
|
```
|
|
|
|
|
|
## Common Pitfalls
|
|
|
|
See also [Error Codes Reference](https://docs.powersync.com/debugging/error-codes.md#error-codes-reference) for PowerSync service error codes.
|
|
|
|
### 1. All children re-render on every table change
|
|
|
|
Without `rowComparator`, every write to a watched table returns a new array — all children re-render even if their row didn't change.
|
|
|
|
```ts
|
|
// BAD — all ListItem components re-render on any lists write
|
|
const { data: lists } = useQuery('SELECT * FROM lists');
|
|
return lists.map(l => <ListItem list={l} />);
|
|
|
|
// GOOD — only changed rows get new references, React.memo skips the rest
|
|
const { data: lists } = useQuery('SELECT * FROM lists', [], {
|
|
rowComparator: { keyBy: r => r.id, compareBy: r => JSON.stringify(r) }
|
|
});
|
|
const ListItem = React.memo(({ list }) => <Text>{list.name}</Text>);
|
|
```
|
|
|
|
### 2. Awaiting connect() thinking data is ready
|
|
|
|
```ts
|
|
// WRONG — connect() is fire-and-forget, data is NOT available after await
|
|
await db.connect(connector);
|
|
renderApp(); // may show empty data
|
|
|
|
// CORRECT
|
|
db.connect(connector);
|
|
await db.waitForFirstSync(); // wait for data
|
|
renderApp();
|
|
```
|
|
|
|
### 3. Schema ID column
|
|
|
|
```ts
|
|
// WRONG
|
|
const todos = new Table({ id: column.text, description: column.text });
|
|
|
|
// RIGHT — 'id' is auto-created by PowerSync
|
|
const todos = new Table({ description: column.text });
|
|
```
|
|
|
|
### 4. uploadData queue stuck
|
|
|
|
If `transaction.complete()` is never called, `getNextCrudTransaction()` returns the same transaction forever. The upload queue stalls permanently. Always call `complete()`, even on partial failure if you want to skip a bad transaction.
|
|
|
|
### 5. Web: SQLite library conflicts
|
|
|
|
If another SQLite package exists in the project (`sql.js`, `better-sqlite3`, etc.), it can conflict with PowerSync's SQLite engine. Remove all other SQLite libraries. Symptom: "Could not load extension" error.
|
|
|
|
### 6. useQuery data seems stale / not updating
|
|
|
|
- Verify the table name in SQL exactly matches the schema key (case-sensitive)
|
|
- Writes must go through `db.execute()` or `writeTransaction()` — writes via raw SQLite connections bypass PowerSync's change tracking
|
|
- Check `db.currentStatus.connected` — if false, sync isn't running
|