Skip to main content
This feature requires Despia V4, currently in beta. Most Despia apps are running on Despia V3. To join the V4 beta, email beta@despia.com.
Query a local SQLite database and sync it with your backend in real time.

Installation

npm install @despia/powersync
import { db } from '@despia/powersync';

API Reference

Connect

Establish a connection to your PowerSync instance. fetchToken is called whenever a new JWT is needed.
await db.connect({
    fetchToken: async () => {
        const res = await fetch('/api/powersync-token')
        const { token } = await res.json()
        return token
    },
    url: 'https://YOUR_POWERSYNC_INSTANCE',
})
fetchToken
() => Promise<string>
required
Returns a short-lived JWT from your backend. Called automatically when the token expires.
url
string
Your PowerSync instance URL.

Migrate

Run schema migrations on startup. The runtime tracks the installed version and only executes statements for versions higher than the current one.
await db.migrate(1, [
    { sql: 'CREATE TABLE IF NOT EXISTS users(id INTEGER PRIMARY KEY, email TEXT)' },
    { sql: 'CREATE TABLE IF NOT EXISTS todos(id INTEGER PRIMARY KEY, title TEXT, done INTEGER DEFAULT 0)' },
])

await db.migrate(2, [
    { sql: 'ALTER TABLE users ADD COLUMN name TEXT' },
])
version
number
required
Version number for this migration. Only runs if the installed version is lower.
statements
BatchStatement[]
required
Array of SQL statements to run for this version.

Query

Fetch multiple rows from local SQLite. Instant, no network.
type User = { id: number; email: string }
const users = await db.query<User>('SELECT id, email FROM users')
sql
string
required
SQL SELECT statement.
params
unknown[]
Optional array of values bound to ? placeholders.

Get

Fetch a single row. Returns null if no match found.
const user = await db.get<User>('SELECT * FROM users WHERE id = ?', [userId])
if (user) console.log(user.email)

Execute

Run a single write statement.
const result = await db.execute(
    'INSERT INTO todos(title, done) VALUES(?, ?)',
    ['Buy milk', 0]
)
// { rowsAffected: 1, insertId: 42 }
rowsAffected
number
Number of rows affected.
insertId
number
Row ID of the last inserted row (INSERT only).

Batch

Run multiple write statements atomically.
await db.batch([
    { sql: 'INSERT INTO users(email) VALUES(?)', params: ['a@b.com'] },
    { sql: 'INSERT INTO users(email) VALUES(?)', params: ['c@d.com'] },
    { sql: 'UPDATE config SET value = ? WHERE key = ?', params: ['ready', 'status'] },
])
results
ExecuteResult[]
Array of results, one per statement.

Transaction

Run a group of statements with full rollback on failure.
await db.transaction(async (tx) => {
    await tx.execute('UPDATE accounts SET balance = balance - ? WHERE id = ?', [100, fromId])
    await tx.execute('UPDATE accounts SET balance = balance + ? WHERE id = ?', [100, toId])
})
If any statement throws, the entire transaction is rolled back.

Watch

Subscribe to a query. Fires the callback immediately with the current result set, then again whenever matching data changes, including changes arriving from sync.
type Todo = { id: number; title: string; done: 0 | 1 }

const unwatch = db.watch<Todo>('SELECT * FROM todos', (rows) => {
    renderTodos(rows)
})

// Stop watching
unwatch()

Sync

Trigger a manual sync.
await db.sync()

Disconnect

Stop the sync engine.
await db.disconnect()

syncStatus

Read the current sync state.
const status = await db.syncStatus()
connected
boolean
Whether the sync engine is connected to the PowerSync instance.
lastSynced
string | null
ISO timestamp of the last successful sync, or null if never synced.
uploading
boolean
Whether local writes are currently being uploaded.
downloading
boolean
Whether data is currently being downloaded from the backend.
{
  "connected": true,
  "lastSynced": "2026-04-01T09:00:00.000Z",
  "uploading": false,
  "downloading": false
}

onSyncChange

Subscribe to sync state changes.
const unsub = db.onSyncChange((status) => {
    if (!status.connected) showOfflineBanner()
    else hideOfflineBanner()
})

// Stop listening
unsub()

Sync flow


React Hook

import { useState, useEffect, useCallback } from 'react'
import { db } from '@despia/powersync'

function useLiveQuery<T extends Record<string, unknown>>(
    sql: string,
    params?: unknown[]
) {
    const [rows, setRows] = useState<T[]>([])

    useEffect(() => {
        const unwatch = params
            ? db.watch<T>(sql, params, setRows)
            : db.watch<T>(sql, setRows)
        return unwatch
    }, [sql, JSON.stringify(params)])

    return rows
}

// Usage
function TodoList() {
    const todos = useLiveQuery<{ id: number; title: string; done: number }>(
        'SELECT * FROM todos WHERE done = ?',
        [0]
    )
    return todos.map(t => <div key={t.id}>{t.title}</div>)
}

TypeScript types

export type ExecuteResult = {
    rowsAffected: number
    insertId?:    number
}

export type BatchStatement = {
    sql:     string
    params?: unknown[]
}

export type BatchResult = {
    results: ExecuteResult[]
}

export type SyncStatus = {
    connected:   boolean
    lastSynced:  string | null
    uploading:   boolean
    downloading: boolean
}

export type ConnectOptions = {
    fetchToken: () => Promise<string>
    url?:       string
}

Environment check

if (navigator.userAgent.includes('despia')) {
    // Use PowerSync
} else {
    // Fallback for non-Despia environment (standard browser)
}
@despia/powersync requires the native bridge and will throw in a standard browser. Gate calls behind this check if your app also runs on web.

Resources

NPM Package

@despia/powersync

GitHub

despia-native/despia-powersync

PowerSync

Backend setup, schema config, and sync rules