Skip to main content

Documentation Index

Fetch the complete documentation index at: https://setup.despia.com/llms.txt

Use this file to discover all available pages before exploring further.

The PowerSync integration is fully built and production-ready. It will become publicly available when the Despia V4 editor launches shortly. To enable it for your app in the meantime, contact Despia customer support at support@despia.com.
PowerSync brings a native local SQLite database to your Despia app, with optional cloud sync to your backend. Queries hit local storage, there is no network round-trip. The app stays fully usable offline, and sync is opt-in when you have credentials.
Local SQLite operations work without any cloud setup. Sync is a separate layer you turn on with db.powersync.connect({ token }) once you have an authenticated user.

Two layers, one API

@despia/powersync exposes two layers under the same db object:
LayerWorks offlineRequires PowerSync cloud setupAPIs
Local SQLiteYesNoinit, query, get, execute, batch, transaction, watch, migrate, schema
Cloud syncYes, sync resumes laterYesdb.powersync.connect, db.powersync.sync, db.powersync.status, db.powersync.events.status
Local database operations do not require a token. Sync starts only after native has an active schema, applied migrations, and credentials from db.powersync.connect({ token }).

Installation

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

Environment check

@despia/powersync requires the native bridge. Use active() to check whether the bridge is available before calling any database methods.
import { active } from '@despia/powersync'

if (!active()) {
    console.warn('Native PowerSync bridge is not available.')
}
active() only checks bridge presence. It does not mean SQLite is initialized or sync is connected.

Why we built this

Despia apps run your existing web app inside a native Swift and Kotlin runtime. Most apps rely on fetching data from a remote API on every load. That works fine on a fast connection. It breaks on slow networks, fails completely offline, and adds latency to every interaction. We wanted local-first data that works the way web developers expect: query with SQL, write normally, get results instantly. No custom sync protocols. No manual conflict resolution. No arbitrary storage limits. So we integrated PowerSync, a production-proven sync engine, directly into the Despia runtime. The result: a native SQLite database on-device, a simple JavaScript API, and optional two-way sync with your backend.

Cross-platform by design

PowerSync is built for both iOS and Android through a unified JavaScript SDK. Write your code once. It works identically on both platforms. No iOS-specific workarounds. No Android edge cases. The API is fully standardised so you can focus on building your app instead of handling platform differences.
import { db } from '@despia/powersync'

// Same code. Both platforms.
type User = { id: string; email: string }
const users = await db.query<User>('SELECT id, email FROM users WHERE active = ?', [1])
Under the hood, Despia handles the platform-specific SQLite implementations so your JavaScript stays clean and portable.

Fully on-device. No cloud dependencies for local data.

Local SQLite runs entirely through the native runtime. Your data lives on the device. Built on native SQLite. Both iOS and Android have SQLite built into the OS. PowerSync uses these native engines directly, no third-party database, no in-memory fallback, no storage quotas. Use any backend. When you turn on sync, PowerSync connects to your existing Postgres, MongoDB, or MySQL database through sync rules. Despia does not sit between you and your data. No per-user fees for local storage. The database is on the device. Storage scales with the device, not with your user count. Works offline completely. Once initial sync completes, the app operates without any network connectivity. Reads and writes work locally. Pending writes are uploaded when connectivity returns.

How it works

You initialize the database with a schema and a schema version, then run migrations to set up the actual SQLite tables. Sync is a separate step that you only call when you have a token for the signed-in user.
import { db, type PowerSyncSchema } from '@despia/powersync'

const SCHEMA_VERSION = 1

const SCHEMA: PowerSyncSchema = {
    users: {
        columns: {
            id:        'text',
            email:     'text',
            createdAt: 'text',
        },
        indexes: {
            users_by_email: ['email'],
        },
    },
    todos: {
        columns: {
            id:        'text',
            userId:    'text',
            title:     'text',
            done:      'integer',
            createdAt: 'text',
        },
        indexes: {
            todos_by_user: ['userId'],
        },
    },
}

await db.init({
    schema:        SCHEMA,
    schemaVersion: SCHEMA_VERSION,
    databaseName:  'mydb',
})
Run migrations on startup to create the actual tables. Schema describes the expected shape, migrations change the SQLite file:
await db.migrate(1, [
    `CREATE TABLE IF NOT EXISTS users (
        id TEXT PRIMARY KEY,
        email TEXT NOT NULL,
        createdAt TEXT NOT NULL
    )`,
    'CREATE INDEX IF NOT EXISTS users_by_email ON users(email)',
    `CREATE TABLE IF NOT EXISTS todos (
        id TEXT PRIMARY KEY,
        userId TEXT NOT NULL,
        title TEXT NOT NULL,
        done INTEGER DEFAULT 0,
        createdAt TEXT NOT NULL
    )`,
    'CREATE INDEX IF NOT EXISTS todos_by_user ON todos(userId)',
])
Query and write immediately, all local, all instant:
// Query
type Todo = { id: string; title: string; done: 0 | 1 }
const todos = await db.query<Todo>('SELECT * FROM todos WHERE done = ?', [0])

// Write
await db.execute(
    'INSERT INTO todos(id, userId, title, done, createdAt) VALUES(?, ?, ?, ?, ?)',
    ['todo_1', 'user_1', 'Buy milk', 0, new Date().toISOString()]
)

// Transaction
await db.transaction(async (tx) => {
    await tx.execute('UPDATE todos SET done = 1 WHERE id = ?', [todoId])
})
Add sync only when you have a token for the signed-in user:
const token = await getPowerSyncToken()
await db.powersync.connect({ token })

Live queries

Subscribe to a query and receive updates whenever the underlying data changes, including changes arriving from sync.
type Todo = { id: string; title: string; done: 0 | 1 }

const unwatch = db.watch<Todo>(
    'SELECT * FROM todos WHERE done = ?',
    [0],
    (rows) => renderTodos(rows)
)

// Stop watching when component unmounts
unwatch()
The callback fires immediately with the current result set, then again whenever a matching row changes. No polling. No manual refresh.

Sync status

Check the current sync state and subscribe to changes:
const status = await db.powersync.status()
// { connected: true, lastSynced: "2026-04-01T09:00:00Z", uploading: false, downloading: false }

const unsubscribe = db.powersync.events.status((status) => {
    updateSyncIndicator(status.connected)
})
Trigger a manual sync at any point:
await db.powersync.sync()
db.powersync.sync() is a trigger. Sync usually completes asynchronously, so read the result with db.powersync.status() or subscribe with db.powersync.events.status().

Startup lifecycle

Use this order on every app start. It guarantees that sync only begins after the schema is active and migrations are applied.
import { db } from '@despia/powersync'
import { CURRENT_SCHEMA, SCHEMA_VERSION, DATABASE_NAME, MIGRATIONS } from './schema'

await db.init({
    schema:        CURRENT_SCHEMA,
    schemaVersion: SCHEMA_VERSION,
    databaseName:  DATABASE_NAME,
})

const activeSchema    = await db.schema().catch(() => null)
const appliedVersion  = activeSchema?.appliedMigrationVersion ?? 0
const pendingMigrations = MIGRATIONS.filter((m) => m.version > appliedVersion)

if (pendingMigrations.length > 0) {
    await db.migrate(
        SCHEMA_VERSION,
        pendingMigrations.flatMap((m) => m.statements)
    )
}

if (token) {
    await db.powersync.connect({ token })
}
Why this order matters:
  • init() gives native the latest schema and target schema version as pending state.
  • schema() returns the last active schema, if native already has one cached.
  • migrate() applies all pending SQL in one transaction.
  • Native promotes pending schema to active only after the migration reaches schemaVersion.
  • db.powersync.connect() starts sync only after active schema and credentials exist.

Use cases

Offline-first apps

Field service, logistics, and inspection apps that must work without connectivity. Data syncs when the device comes back online.

Instant UI

No loading spinners waiting for API responses. Every read hits local SQLite in milliseconds regardless of network conditions.

Real-time collaboration

Subscribe to live queries and reflect changes from other users the moment they arrive via sync.

Data-heavy apps

Store large datasets locally without hitting browser storage quotas. SQLite handles millions of rows efficiently.

Frequently asked questions

Does this work offline?

Yes. Queries and writes hit local SQLite. Once initial sync completes, the app works fully offline. Pending writes are queued and uploaded when connectivity returns.

Do I need a token to use the local database?

No. Local SQLite operations (init, query, get, execute, batch, transaction, watch, migrate) work without any token. You only need a token when you call db.powersync.connect({ token }) to start cloud sync.
Native reads the static PowerSync app ID and instance URL from native config. You only pass a user-scoped JWT token to db.powersync.connect({ token }). The token identifies the signed-in user, native owns the rest of the connection setup.
PowerSync syncs with Postgres, MongoDB, and MySQL via sync rules you configure in the PowerSync dashboard. Despia handles the native bridge, the backend connection is between PowerSync and your database.
Yes. PowerSync is compatible with both Remote Hydration (default) and Local Server (http://localhost). The database runs in the native layer regardless of how the web app is served.
Yes. The database is compiled into the app binary. No executables are downloaded post-install. SQLite is a standard system framework on both iOS and Android.
The integration is fully built and ready. It will become publicly available when the V4 editor launches. In the meantime, contact support@despia.com to enable it on your app.

Resources

NPM Package

@despia/powersync

Reference

Full API, init, query, execute, batch, watch, migrate, sync

PowerSync

Backend setup, schema config, and sync rules