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' ;
< script src = "https://cdn.jsdelivr.net/npm/@despia/powersync/dist/umd/despia-powersync.min.js" ></ script >
< script >
const { db } = window . DespiaPowerSync
</ script >
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.
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 for this migration. Only runs if the installed version is lower.
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' )
const active = await db . query < User >(
'SELECT id, email FROM users WHERE active = ? AND role = ?' ,
[ 1 , 'admin' ]
)
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 }
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' ] },
])
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.
Without params
With params
type Todo = { id : number ; title : string ; done : 0 | 1 }
const unwatch = db . watch < Todo >( 'SELECT * FROM todos' , ( rows ) => {
renderTodos ( rows )
})
// Stop watching
unwatch ()
const unwatch = db . watch < Todo >(
'SELECT * FROM todos WHERE done = ?' ,
[ 0 ],
( rows ) => renderTodos ( rows )
)
Sync
Trigger a manual sync.
Disconnect
Stop the sync engine.
syncStatus
Read the current sync state.
const status = await db . syncStatus ()
Whether the sync engine is connected to the PowerSync instance.
ISO timestamp of the last successful sync, or null if never synced.
Whether local writes are currently being uploaded.
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