Skip to main content

1. Configure Your Source Database

PowerSync needs to connect to your source database (Postgres, MongoDB, MySQL, SQL Server or Convex) to replicate data. Before setting up PowerSync, you need to configure your database with the appropriate permissions and replication settings.
Using the PowerSync CLI and want an automatically integrated Postgres instance for local development? You can skip to Step 2 and set one up with the CLI (Self-Hosted) tab.
Configuring Postgres for PowerSync involves three main tasks:
  1. Enable logical replication: PowerSync reads the Postgres write-ahead log (WAL) using logical replication. Set wal_level = logical in your Postgres configuration.
  2. Create a PowerSync database user: Create a role with replication privileges and read-only access to your tables.
  3. Create a powersync publication: Create a logical replication publication named powersync to specify which tables to replicate.
-- 1. Enable logical replication (requires restart)
ALTER SYSTEM SET wal_level = logical;

-- 2. Create PowerSync database user/role with replication privileges and read-only access to your tables
CREATE ROLE powersync_role WITH REPLICATION BYPASSRLS LOGIN PASSWORD 'myhighlyrandompassword';

-- Set up permissions for the newly created role
-- Read-only (SELECT) access is required
GRANT SELECT ON ALL TABLES IN SCHEMA public TO powersync_role;

-- Optionally, grant SELECT on all future tables (to cater for schema additions)
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO powersync_role;

-- 3. Create a publication to replicate tables. The publication must be named "powersync"
CREATE PUBLICATION powersync FOR ALL TABLES;
-- Supabase has logical replication enabled by default
-- Just create the user and publication:

-- Create PowerSync database user/role with replication privileges and read-only access to your tables
CREATE ROLE powersync_role WITH REPLICATION BYPASSRLS LOGIN PASSWORD 'myhighlyrandompassword';

-- Set up permissions for the newly created role
-- Read-only (SELECT) access is required
GRANT SELECT ON ALL TABLES IN SCHEMA public TO powersync_role;

-- Optionally, grant SELECT on all future tables (to cater for schema additions)
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO powersync_role;

-- Create a publication to replicate tables. The publication must be named "powersync"
CREATE PUBLICATION powersync FOR ALL TABLES;
# 1. Create a Docker network (if not already created)
# This allows various PowerSync containers to communicate with each other
docker network create powersync-network

# 2. Run Postgres source database with logical replication enabled (required for PowerSync)
docker run -d \
  --name powersync-postgres \
  --network powersync-network \
  -e POSTGRES_PASSWORD="my_secure_password" \
  -p 5432:5432 \
  postgres:18 \
  postgres -c wal_level=logical

# 3. Configure PowerSync user and publication
# This creates a PowerSync database user/role with replication privileges and read-only access to your tables
# Read-only (SELECT) access is also granted to all future tables (to cater for schema additions)
# It also creates a publication to replicate tables. The publication must be named "powersync"
docker exec -it powersync-postgres psql -U postgres -c "
CREATE ROLE powersync_role WITH REPLICATION BYPASSRLS LOGIN PASSWORD 'myhighlyrandompassword';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO powersync_role;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO powersync_role;
CREATE PUBLICATION powersync FOR ALL TABLES;"
PowerSync requires Postgres version 11 or greater.
Learn More
  • For more details on Postgres setup, including provider-specific guides (Supabase, AWS RDS, etc.), see Source Database Setup.
  • Self-hosting PowerSync? See the Self-Host-Demo App for a complete working example of connecting a Postgres source database to PowerSync.

2. Set Up PowerSync Service Instance

PowerSync is available as a cloud-hosted service (PowerSync Cloud) or can be self-hosted (PowerSync Open Edition or PowerSync Enterprise Self-Hosted Edition).
If you haven’t yet, sign up for a free PowerSync Cloud account here.After signing up, you will be taken to the PowerSync Dashboard.Here, create a new project. PowerSync creates Development and Production instances of the PowerSync Service by default in the project.

3. Connect PowerSync to Your Source Database

The next step is to connect your PowerSync Service instance to your source database.
In the PowerSync Dashboard, select your project and instance, then go to Database Connections:
  1. Click Connect to Source Database
  2. Select the appropriate database type tab (Postgres, MongoDB, MySQL, SQL Server or Convex)
  3. Fill in your connection details:
    Use the username (e.g., powersync_role) and password you created in Step 1: Configure your Source Database.
    • Postgres: Host, Port (5432), Database name, Username, Password, SSL Mode
    • MongoDB: Connection URI (e.g., mongodb+srv://user:pass@cluster.mongodb.net/database)
    • MySQL: Host, Port (3306), Database name, Username, Password
    • Convex: Deployment URL and deploy key. See Convex Source Database Setup.
    • SQL Server: Name, Host, Port (1433), Database name, Username, Password
  4. Click Test Connection to verify
  5. Click Save Connection
PowerSync will now deploy and configure an isolated cloud environment, which can take a few minutes.
Learn MoreFor more details on database connections, including provider-specific connection details (Supabase, AWS RDS, MongoDB Atlas, etc.), see Source Database Connection.

4. Define Sync Streams

PowerSync uses Sync Streams to control which data gets synced to which users/devices, using SQL-like queries defined in YAML format. Start with simple auto-subscribed streams that sync data to all users by default:
config:
  edition: 3
streams:
  shared_data:
    auto_subscribe: true
    queries:
      - SELECT * FROM todos
      - SELECT * FROM lists WHERE NOT archived
config:
  edition: 3
streams:
  shared_data:
    auto_subscribe: true
    # MongoDB uses "_id" but PowerSync uses "id" on the client
    queries:
      - SELECT *, _id as id FROM lists
      - SELECT *, _id as id FROM todos WHERE archived = false
config:
  edition: 3
streams:
  shared_data:
    auto_subscribe: true
    queries:
      - SELECT * FROM todos
      - SELECT * FROM lists WHERE NOT archived
config:
  edition: 3
streams:
  shared_data:
    auto_subscribe: true
    queries:
      - SELECT * FROM todos
      - SELECT * FROM lists WHERE NOT archived
Learn more: Sync Streams documentation

Deploy Your Configuration

In the PowerSync Dashboard:
  1. Select your project and instance
  2. Go to the Sync Streams view
  3. Edit the YAML directly in the dashboard
  4. Click Deploy to validate and deploy your Sync Streams
Table/collection names in your configuration must match the table names defined in your client-side schema (defined in a later step below).

5. Generate a Development Token

For quick development and testing, you can generate a temporary development token instead of implementing full authentication. You’ll use this token for two purposes:
  • Testing with the Sync Diagnostics Client (in the next step) to verify your setup and Sync Streams
  • Connecting your app (in a later step) to test the client SDK integration
  1. In the PowerSync Dashboard, select your project and instance
  2. Go to the Client Auth view
  3. Check the Development tokens setting and save your changes
  4. Click the Connect button in the top bar
  5. Enter a token subject: Since you’re starting with simple streams or buckets that sync all data to all users (as recommended in the previous step), you can put something like test-user as the token subject (which would normally be the user ID you want to test with).
  6. Click Generate token and copy the token
Development tokens expire after 12 hours.

6. [Optional] Test Sync with the Sync Diagnostics Client

Before implementing the PowerSync Client SDK in your app, you can validate that syncing is working correctly using our Sync Diagnostics Client (this hosted version works with both PowerSync Cloud and self-hosted setups). Use the development token you generated in the previous step to connect and verify your setup:
  1. Go to https://diagnostics-app.powersync.com
  2. Enter your development token at PowerSync Token (from the Generate a Development Token step above)
  3. Enter your PowerSync instance URL at PowerSync Endpoint (found in the PowerSync Dashboard - click Connect in the top bar)
  4. Click Proceed
The Sync Diagnostics Client will connect to your PowerSync Service instance and display information about the synced data, and allow you to query the client-side SQLite database.
Checkpoint:Inspect your synced tables in the Sync Diagnostics Client. These should match the Sync Streams you defined previously. This confirms your setup is working correctly before integrating the client SDK into your app.

7. Use the Client SDK

Now it’s time to integrate PowerSync into your app. This involves installing the Client SDK, defining your client-side schema, instantiating the database, connecting to your PowerSync Service instance, and reading/writing data.

Install the Client SDK

Add the PowerSync Client SDK to your app project. PowerSync provides SDKs for various platforms and frameworks.
Add the PowerSync React Native NPM package to your project:
npx expo install @powersync/react-native
Install Peer DependenciesPowerSync requires a SQLite database adapter. Choose between:
Using Expo Go? Our native database adapters listed below (OP-SQLite and React Native Quick SQLite) are not compatible with Expo Go’s sandbox environment. To run PowerSync with Expo Go install our JavaScript-based adapter @powersync/adapter-sql-js instead. See details here.
Polyfills and additional notes:
  • For async iterator support with watched queries, additional polyfills are required. See the Babel plugins section in the README.
  • When using the OP-SQLite package, we recommend adding this metro config to avoid build issues.

Define Your Client-Side Schema

This refers to the for the managed SQLite database exposed by the PowerSync Client SDKs, that your app can read from and write to. The schema is applied when the database is instantiated (as we’ll show in the next step) — . PowerSync Cloud: The easiest way to generate your schema is using the PowerSync Dashboard. Click the Connect button in the top bar to generate the client-side schema based on your Sync Streams in your preferred language. Here’s an example schema for a simple todos table:
import { column, Schema, Table } from '@powersync/react-native';

const todos = new Table(
  {
    list_id: column.text,
    created_at: column.text,
    completed_at: column.text,
    description: column.text,
    created_by: column.text,
    completed_by: column.text,
    completed: column.integer
  },
  { indexes: { list: ['list_id'] } }
);

export const AppSchema = new Schema({
  todos
});
import { column, Schema, Table } from '@powersync/web';

const todos = new Table(
  {
    list_id: column.text,
    created_at: column.text,
    completed_at: column.text,
    description: column.text,
    created_by: column.text,
    completed_by: column.text,
    completed: column.integer
  },
  { indexes: { list: ['list_id'] } }
);

export const AppSchema = new Schema({
  todos
});
import { column, Schema, Table } from '@powersync/common';

const todos = new Table(
  {
    list_id: column.text,
    created_at: column.text,
    completed_at: column.text,
    description: column.text,
    created_by: column.text,
    completed_by: column.text,
    completed: column.integer
  },
  { indexes: { list: ['list_id'] } }
);

export const AppSchema = new Schema({
  todos
});
import { column, Schema, Table } from '@powersync/node';

const todos = new Table(
  {
    list_id: column.text,
    created_at: column.text,
    completed_at: column.text,
    description: column.text,
    created_by: column.text,
    completed_by: column.text,
    completed: column.integer
  },
  { indexes: { list: ['list_id'] } }
);

export const AppSchema = new Schema({
  todos
});
import com.powersync.db.schema.Column
import com.powersync.db.schema.Schema
import com.powersync.db.schema.Table
import com.powersync.db.schema.Index
import com.powersync.db.schema.IndexedColumn

val AppSchema: Schema = Schema(
  listOf(
    Table(
      name = "todos",
      columns = listOf(
        Column.text("list_id"),
        Column.text("created_at"),
        Column.text("completed_at"),
        Column.text("description"),
        Column.integer("completed"),
        Column.text("created_by"),
        Column.text("completed_by")
      ),
      indexes = listOf(
        Index("list", listOf(IndexedColumn.descending("list_id")))
      )
    )
  )
)
import PowerSync

let todos = Table(
  name: "todos",
  columns: [
    Column.text("list_id"),
    Column.text("description"),
    Column.integer("completed"),
    Column.text("created_at"),
    Column.text("completed_at"),
    Column.text("created_by"),
    Column.text("completed_by")
  ],
  indexes: [
    Index(
      name: "list_id",
      columns: [IndexedColumn.ascending("list_id")]
    )
  ]
)

let AppSchema = Schema(todos)
import 'package:powersync/powersync.dart';

const schema = Schema(([
  Table('todos', [
    Column.text('list_id'),
    Column.text('created_at'),
    Column.text('completed_at'),
    Column.text('description'),
    Column.integer('completed'),
    Column.text('created_by'),
    Column.text('completed_by'),
  ], indexes: [
    Index('list', [IndexedColumn('list_id')])
  ])
]));
using PowerSync.Common.DB.Schema;
using PowerSync.Common.DB.Schema.Attributes;

[Table("todos"), Index("list", ["list_id"])]
public class Todo
{
    // Attribute-based schema requires an explicit id; other syntaxes define an implicit id key. Learn more in the .NET SDK reference.
    [Column("id")]
    public string TodoId { get; set; }

    [Column("list_id")]
    public string ListId { get; set; }

    [Column("created_at")]
    public string CreatedAt { get; set; }

    [Column("completed_at")]
    public string CompletedAt { get; set; }

    [Column("description")]
    public string Description { get; set; }

    [Column("created_by")]
    public string CreatedBy { get; set; }

    [Column("completed_by")]
    public string CompletedBy { get; set; }

    [Column("completed")]
    public bool Completed { get; set; }
}

public static Schema PowerSyncSchema = new Schema(typeof(Todo));
This uses the recommended attribute-based syntax, where your C# class doubles as both the schema definition and the result type for queries — so you only define your data structure once. If you prefer to keep your schema definition separate from your data classes, an object initializer syntax is also available. See the .NET SDK reference for details.
use powersync::schema::{Column, Schema, Table};

pub fn app_schema() -> Schema {
    let mut schema = Schema::default();
    let todos = Table::create(
        "todos",
        vec![
            Column::text("list_id"),
            Column::text("created_at"),
            Column::text("completed_at"),
            Column::text("description"),
            Column::integer("completed"),
            Column::text("created_by"),
            Column::text("completed_by"),
        ],
        |_| {},
    );
    schema.tables.push(todos);
    schema
}
Note: The schema does not explicitly specify an id column, since PowerSync automatically creates an id column of type text. PowerSync recommends using UUIDs.
Learn MoreThe client-side schema uses three column types: text, integer, and real. These map directly to values from your Sync Streams and are automatically cast if needed. For details on how backend database types map to SQLite types, see Types.

Instantiate the PowerSync Database

Now that you have your client-side schema defined, instantiate the PowerSync database in your app. This creates the client-side SQLite database that PowerSync keeps in sync with your source database based on your Sync Streams.
import { PowerSyncDatabase } from '@powersync/react-native';
import { AppSchema } from './Schema';

export const db = new PowerSyncDatabase({
  schema: AppSchema,
  database: {
    dbFilename: 'powersync.db'
  }
});
import { PowerSyncDatabase } from '@powersync/web';
import { AppSchema } from './Schema';

export const db = new PowerSyncDatabase({
  schema: AppSchema,
  database: {
    dbFilename: 'powersync.db'
  }
});
import { PowerSyncDatabase } from '@powersync/node';
import { AppSchema } from './Schema';

export const db = new PowerSyncDatabase({
  schema: AppSchema,
  database: {
    dbFilename: 'powersync.db'
  }
});
import { PowerSyncTauriDatabase } from '@powersync/tauri-plugin';
import { appDataDir } from '@tauri-apps/api/path';
import { AppSchema } from './AppSchema';

export const db = new PowerSyncTauriDatabase({
  schema: AppSchema,
  database: {
    dbFilename: 'powersync.db',
    // Store the database in the app data directory
    dbLocationAsync: appDataDir,
  }
});
import { PowerSyncDatabase } from '@powersync/capacitor';

// Import general components from the Web SDK package
import { Schema } from '@powersync/web';
import { Connector } from './Connector';
import { AppSchema } from './AppSchema';

/**
 * The Capacitor PowerSyncDatabase will automatically detect the platform
 * and use the appropriate database drivers.
 */
export const db = new PowerSyncDatabase({
  // The schema you defined in the previous step
  schema: AppSchema,
  database: {
    // Filename for the SQLite database — it's important to only instantiate one instance per file.
    dbFilename: 'powersync.db'
  }
});
import com.powersync.DatabaseDriverFactory
import com.powersync.PowerSyncDatabase

// Android
val driverFactory = DatabaseDriverFactory(this)
// iOS & Desktop
// val driverFactory = DatabaseDriverFactory()

val database = PowerSyncDatabase({
  factory: driverFactory,
  schema: AppSchema,
  dbFilename: "powersync.db"
})
import PowerSync

let db = PowerSyncDatabase(
  schema: AppSchema,
  dbFilename: "powersync.sqlite"
)
import 'package:powersync/powersync.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart';

openDatabase() async {
  final dir = await getApplicationSupportDirectory();
  final path = join(dir.path, 'powersync-dart.db');
  db = PowerSyncDatabase(schema: schema, path: path);
  await db.initialize();
}
using PowerSync.Common.Client;

class Demo
{
    static async Task Main()
    {
        var db = new PowerSyncDatabase(new PowerSyncDatabaseOptions
        {
            Database = new SQLOpenOptions { DbFilename = "tododemo.db" },
            Schema = AppSchema.PowerSyncSchema,
        });
        await db.Init();
    }
}
using PowerSync.Common.Client;
using PowerSync.Common.MDSQLite;
using PowerSync.Maui.SQLite;

class Demo
{
    static async Task Main() 
    {
        // Ensures the DB file is stored in a platform appropriate location
        var dbPath = Path.Combine(FileSystem.AppDataDirectory, "maui-example.db");
        var factory = new MAUISQLiteDBOpenFactory(new MDSQLiteOpenFactoryOptions()
        {
            DbFilename = dbPath
        });

        var Db = new PowerSyncDatabase(new PowerSyncDatabaseOptions()
        {
            Database = factory, // Supply a factory
            Schema = AppSchema.PowerSyncSchema,
        });

        await db.Init();
    }
}
// 1. Process setup: register PowerSync extension early (e.g. in main()).
// 2. Open a connection pool, create env, then database. Spawn async tasks
//    before connecting (see Connect step). Requires powersync with tokio feature.

use powersync::{ConnectionPool, PowerSyncDatabase, error::PowerSyncError};
use powersync::env::PowerSyncEnvironment;
use std::sync::Arc;
use http_client::IsahcClient;

fn open_pool() -> Result<ConnectionPool, PowerSyncError> {
    ConnectionPool::open("powersync.db")
}

// This example shows the Tokio runtime. You must call 
// `PowerSyncEnvironment::powersync_auto_extension()` before using the SDK and spawn async 
// tasks with `db.async_tasks().spawn_with_tokio()` (or `spawn_with` for other runtimes) 
// before connecting. See the Rust SDK reference for in-memory pools, smol, or custom runtimes.

#[tokio::main]
async fn main() {
    PowerSyncEnvironment::powersync_auto_extension()
        .expect("could not load PowerSync core extension");

    let pool = open_pool().expect("open pool");
    let client = Arc::new(IsahcClient::new());
    let env = PowerSyncEnvironment::custom(
        client.clone(),
        pool,
        Box::new(PowerSyncEnvironment::tokio_timer()),
    );

    let db = PowerSyncDatabase::new(env, app_schema());
    db.async_tasks().spawn_with_tokio();
    // Connect with a backend connector in the next step.
}

Connect to PowerSync Service Instance

Connect your client-side PowerSync database to the PowerSync Service instance you created in step 2 by defining a backend connector and calling connect(). The backend connector handles authentication and uploading mutations to your backend.
Note: This section assumes you want to use PowerSync to sync your backend source database with SQLite in your app. If you only want to use PowerSync to manage your local SQLite database without sync, instantiate the PowerSync database without calling connect() and refer to our Local-Only guide.
You don’t have to worry about the backend connector implementation details right now. You can leave the boilerplate as-is and come back to it later.
For development, you can use the development token you generated in the Generate a Development Token step above. For production, you’ll implement proper JWT authentication as explained further below.
import { AbstractPowerSyncDatabase, PowerSyncBackendConnector, PowerSyncCredentials } from '@powersync/react-native';
import { db } from './Database';

class Connector implements PowerSyncBackendConnector {
  async fetchCredentials(): Promise<PowerSyncCredentials> {
    // for development: use development token
    return {
      endpoint: 'https://your-instance.powersync.com',
      token: 'your-development-token-here'
    };
  }

  async uploadData(database: AbstractPowerSyncDatabase) {
    const transaction = await database.getNextCrudTransaction();
    if (!transaction) return;

    for (const op of transaction.crud) {
      const record = { ...op.opData, id: op.id };
      // upload to your backend API
    }

    await transaction.complete();
  }
}

// connect the database to PowerSync Service
const connector = new Connector();
await db.connect(connector);
import { AbstractPowerSyncDatabase, PowerSyncBackendConnector, PowerSyncCredentials } from '@powersync/web';
import { db } from './Database';

class Connector implements PowerSyncBackendConnector {
  async fetchCredentials(): Promise<PowerSyncCredentials> {
    // for development: use development token
    return {
      endpoint: 'https://your-instance.powersync.com',
      token: 'your-development-token-here'
    };
  }

  async uploadData(database: AbstractPowerSyncDatabase) {
    const transaction = await database.getNextCrudTransaction();
    if (!transaction) return;

    for (const op of transaction.crud) {
      const record = { ...op.opData, id: op.id };
      // upload to your backend API
    }

    await transaction.complete();
  }
}

// connect the database to PowerSync Service
const connector = new Connector();
await db.connect(connector);
import { PowerSyncBackendConnector } from '@powersync/node';

export class Connector implements PowerSyncBackendConnector {
    async fetchCredentials() {
        // for development: use development token
        return {
            endpoint: 'https://your-instance.powersync.com',
            token: 'your-development-token-here'
        };
    }

    async uploadData(database) {
      // upload to your backend API
    }
}

// connect the database to PowerSync Service
const connector = new Connector();
await db.connect(connector);
import com.powersync.PowerSyncCredentials
import com.powersync.PowerSyncDatabase

class MyConnector : PowerSyncBackendConnector {
  override suspend fun fetchCredentials(): PowerSyncCredentials {
    // for development: use development token
    return PowerSyncCredentials(
      endpoint = "https://your-instance.powersync.com",
      token = "your-development-token-here"
    )
  }

  override suspend fun uploadData(database: PowerSyncDatabase) {
    val transaction = database.getNextCrudTransaction() ?: return
    
    for (op in transaction.crud) {
      val record = op.opData + ("id" to op.id)
      // upload to your backend API
    }
    
    transaction.complete()
  }
}

// connect the database to PowerSync Service
database.connect(MyConnector())
import PowerSync

class Connector: PowerSyncBackendConnector {
  func fetchCredentials() async throws -> PowerSyncCredentials {
    // for development: use development token
    return PowerSyncCredentials(
      endpoint: "https://your-instance.powersync.com",
      token: "your-development-token-here"
    )
  }

  func uploadData(database: PowerSyncDatabase) async throws {
    guard let transaction = try await database.getNextCrudTransaction() else {
      return
    }
    
    for op in transaction.crud {
      var record = op.opData
      record["id"] = op.id
      // upload to your backend API
    }
    
    try await transaction.complete()
  }
}

// connect the database to PowerSync Service
let connector = Connector()
await db.connect(connector: connector)
import 'package:powersync/powersync.dart';

class Connector extends PowerSyncBackendConnector {
  @override
  Future<PowerSyncCredentials> fetchCredentials() async {
    return PowerSyncCredentials(
      endpoint: 'https://your-instance.powersync.com',
      token: 'your-development-token-here'
    );
  }

  @override
  Future<void> uploadData(PowerSyncDatabase database) async {
    final transaction = await database.getNextCrudTransaction();
    if (transaction == null) return;

    for (final op in transaction.crud) {
      final record = {...op.opData, 'id': op.id};
      // upload to your backend API
    }

    await transaction.complete();
  }
}

// connect the database to PowerSync Service
final connector = Connector();
await db.connect(connector);
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using PowerSync.Common.Client;
using PowerSync.Common.Client.Connection;
using PowerSync.Common.DB.Crud;

public class MyConnector : IPowerSyncBackendConnector
{
    public MyConnector()
    {
    }

    public async Task<PowerSyncCredentials?> FetchCredentials()
    {
        var powerSyncUrl = "https://your-instance.powersync.com";
        var authToken = "your-development-token-here";

        // Return credentials with PowerSync endpoint and JWT token
        return new PowerSyncCredentials(powerSyncUrl, authToken);
    }

    public async Task UploadData(IPowerSyncDatabase database)
    {
        // upload to your backend API
    }
}

// connect the database to PowerSync Service
await db.Connect(new MyConnector());
use async_trait::async_trait;
use powersync::{BackendConnector, PowerSyncCredentials, PowerSyncDatabase, SyncOptions};
use powersync::error::PowerSyncError;
use std::sync::Arc;

struct MyBackendConnector {
    client: Arc<dyn http_client::HttpClient>,
    db: PowerSyncDatabase,
}

#[async_trait]
impl BackendConnector for MyBackendConnector {
    async fn fetch_credentials(&self) -> Result<PowerSyncCredentials, PowerSyncError> {
        // for development: use development token
        Ok(PowerSyncCredentials {
            endpoint: "https://your-instance.powersync.com".to_string(),
            token: "your-development-token-here".to_string(),
        })
    }

    async fn upload_data(&self) -> Result<(), PowerSyncError> {
        let mut local_writes = self.db.crud_transactions();
        while let Some(tx) = local_writes.try_next().await? {
            // upload to your backend API
            tx.complete().await?;
        }
        Ok(())
    }
}

// connect the database to PowerSync Service
db.connect(SyncOptions::new(MyBackendConnector {
    client,
    db: db.clone(),
}))
.await;
// For Tauri, connecting to PowerSync must be done in Rust so that sync state
// is shared across all windows. Calling connect() from JavaScript will throw.
//
// 1. Define your backend connector in Rust:

use async_trait::async_trait;
use tauri_plugin_powersync::PowerSyncExt;
use powersync::{BackendConnector, PowerSyncCredentials, PowerSyncDatabase, SyncOptions};
use powersync::error::PowerSyncError;

struct MyBackendConnector {
    db: PowerSyncDatabase,
}

#[async_trait]
impl BackendConnector for MyBackendConnector {
    async fn fetch_credentials(&self) -> Result<PowerSyncCredentials, PowerSyncError> {
        // for development: use development token
        Ok(PowerSyncCredentials {
            endpoint: "https://your-instance.powersync.com".to_string(),
            token: "your-development-token-here".to_string(),
        })
    }

    async fn upload_data(&self) -> Result<(), PowerSyncError> {
        let mut local_writes = self.db.crud_transactions();
        while let Some(tx) = local_writes.try_next().await? {
            // upload to your backend API
            tx.complete().await?;
        }
        Ok(())
    }
}

// 2. Add a Tauri command that receives the database handle from JavaScript:
#[tauri::command]
async fn connect<R: tauri::Runtime>(
    app: tauri::AppHandle<R>,
    handle: usize,
) -> tauri_plugin_powersync::Result<()> {
    let database = app.powersync().database_from_javascript_handle(handle)?;
    let options = SyncOptions::new(MyBackendConnector { db: database.clone() });
    database.connect(options).await;
    Ok(())
}

// 3. Register the command in your Tauri builder:
// .invoke_handler(tauri::generate_handler![connect])

// 4. Then call it from JavaScript after initializing the database:
// await db.init();
// await invoke('connect', { handle: db.rustHandle });
Once connected, you can read from and write to the client-side SQLite database. PowerSync automatically syncs changes from your source database down into the SQLite database. For client-side mutations to be uploaded back to your source database, you need to complete the backend integration as explained below.

Read Data

Read data using SQL queries. The data comes from your client-side SQLite database:
// Get all todos
const todos = await db.getAll('SELECT * FROM todos');

// Get a single todo
const todo = await db.get('SELECT * FROM todos WHERE id = ?', [todoId]);

// Watch for changes (reactive query)
const stream = db.watch('SELECT * FROM todos WHERE list_id = ?', [listId]);
for await (const todos of stream) {
  // Update UI when data changes
  console.log(todos);
}

// Note: The above example requires async iterator support in React Native. 
// If you encounter issues, use one of these callback-based APIs instead:

// Option 1: Using onResult callback
// const abortController = new AbortController();
// db.watch(
//   'SELECT * FROM todos WHERE list_id = ?',
//   [listId],
//   {
//     onResult: (todos) => {
//       // Update UI when data changes
//       console.log(todos);
//     }
//   },
//   { signal: abortController.signal }
// );

// Option 2: Using the query builder API
// const query = db
//   .query({
//     sql: 'SELECT * FROM todos WHERE list_id = ?',
//     parameters: [listId]
//   })
//   .watch();
// query.registerListener({
//   onData: (todos) => {
//     // Update UI when data changes
//     console.log(todos);
//   }
// });
// Get all todos
val todos = database.getAll("SELECT * FROM todos") { cursor ->
  Todo.fromCursor(cursor)
}

// Get a single todo
val todo = database.get("SELECT * FROM todos WHERE id = ?", listOf(todoId)) { cursor ->
  Todo.fromCursor(cursor)
}

// Watch for changes
database.watch("SELECT * FROM todos WHERE list_id = ?", listOf(listId))
  .collect { todos ->
    // Update UI when data changes
  }
// Get all todos
let todos = try await db.getAll(
  sql: "SELECT * FROM todos",
  mapper: { cursor in
    TodoContent(
      description: try cursor.getString(name: "description")!,
      completed: try cursor.getBooleanOptional(name: "completed")
    )
  }
)

// Watch for changes
for try await todos in db.watch(
  sql: "SELECT * FROM todos WHERE list_id = ?",
  parameters: [listId]
) {
  // Update UI when data changes
}
// Get all todos
final todos = await db.getAll('SELECT * FROM todos');

// Get a single todo
final todo = await db.get('SELECT * FROM todos WHERE id = ?', [todoId]);

// Watch for changes
db.watch('SELECT * FROM todos WHERE list_id = ?', [listId])
  .listen((todos) {
    // Update UI when data changes
  });
// Define a result type with properties matching schema columns (some columns omitted for brevity)
// public class ListResult { public string id; public string name; public string owner_id; public string created_at; ... }

// Use db.Get() to fetch a single row:
var list = await db.Get<ListResult>("SELECT * FROM lists WHERE id = ?", [listId]);

// Use db.GetAll() to fetch all rows:
var lists = await db.GetAll<ListResult>("SELECT * FROM lists");

// Watch for changes to query results
var query = await db.Watch("SELECT * FROM lists", null, new WatchHandler<ListResult>
{
    OnResult = (results) => Console.WriteLine($"Lists updated: {results.Length} items"),
    OnError = (error) => Console.WriteLine($"Error: {error.Message}")
});

// Call query.Dispose() to stop watching for updates
query.Dispose();
use rusqlite::params;
use futures::StreamExt; // for try_next() on the watch stream

// Get all todos
async fn get_all_todos(db: &PowerSyncDatabase) -> Result<(), PowerSyncError> {
    let reader = db.reader().await?;
    let mut stmt = reader.prepare("SELECT * FROM todos")?;
    let mut rows = stmt.query(params![])?;
    while let Some(row) = rows.next()? {
        let id: String = row.get("id")?;
        let description: String = row.get("description")?;
        // use row data
    }
    Ok(())
}

// Get a single todo
async fn find_todo(db: &PowerSyncDatabase, todo_id: &str) -> Result<(), PowerSyncError> {
    let reader = db.reader().await?;
    let mut stmt = reader.prepare("SELECT * FROM todos WHERE id = ?")?;
    let mut rows = stmt.query(params![todo_id])?;
    while let Some(row) = rows.next()? {
        let id: String = row.get("id")?;
        let description: String = row.get("description")?;
        println!("Found todo: {id}, {description}");
    }
    Ok(())
}

// Watch for changes
async fn watch_todos(db: &PowerSyncDatabase, list_id: &str) -> Result<(), PowerSyncError> {
    let stream = db.watch_statement(
        "SELECT * FROM todos WHERE list_id = ?".to_string(),
        params![list_id],
        |stmt, params| {
            let mut rows = stmt.query(params)?;
            let mut mapped = vec![];
            while let Some(row) = rows.next()? {
                mapped.push(() /* TODO: Read row into struct */);
            }
            Ok(mapped)
        },
    );
    let mut stream = std::pin::pin!(stream);
    while let Some(_event) = stream.try_next().await? {
        // Update UI when data changes
    }
    Ok(())
}
Learn More

Write Data

Write data using SQL INSERT, UPDATE, or DELETE statements. PowerSync automatically queues these mutations and uploads them to your backend via the uploadData() function, once you’ve fully implemented your backend connector (as explained below).
// Insert a new todo
await db.execute(
  'INSERT INTO todos (id, created_at, list_id, description) VALUES (uuid(), date(), ?, ?)',
  [listId, 'Buy groceries']
);

// Update a todo
await db.execute(
  'UPDATE todos SET completed = 1, completed_at = date() WHERE id = ?',
  [todoId]
);

// Delete a todo
await db.execute('DELETE FROM todos WHERE id = ?', [todoId]);
// Insert a new todo
database.writeTransaction { ctx ->
  ctx.execute(
    sql = "INSERT INTO todos (id, created_at, list_id, description) VALUES (uuid(), date(), ?, ?)",
    parameters = listOf(listId, "Buy groceries")
  )
}

// Update a todo
database.execute(
  sql = "UPDATE todos SET completed = 1, completed_at = date() WHERE id = ?",
  parameters = listOf(todoId)
)

// Delete a todo
database.execute(
  sql = "DELETE FROM todos WHERE id = ?",
  parameters = listOf(todoId)
)
// Insert a new todo
try await db.execute(
  sql: "INSERT INTO todos (id, created_at, list_id, description) VALUES (uuid(), date(), ?, ?)",
  parameters: [listId, "Buy groceries"]
)

// Update a todo
try await db.execute(
  sql: "UPDATE todos SET completed = 1, completed_at = date() WHERE id = ?",
  parameters: [todoId]
)

// Delete a todo
try await db.execute(
  sql: "DELETE FROM todos WHERE id = ?",
  parameters: [todoId]
)
// Insert a new todo
await db.execute(
  'INSERT INTO todos (id, created_at, list_id, description) VALUES (uuid(), date(), ?, ?)',
  [listId, 'Buy groceries']
);

// Update a todo
await db.execute(
  'UPDATE todos SET completed = 1, completed_at = date() WHERE id = ?',
  [todoId]
);

// Delete a todo
await db.execute('DELETE FROM todos WHERE id = ?', [todoId]);
// Insert a new todo
await db.Execute(
  "INSERT INTO todos (id, created_at, list_id, description) VALUES (uuid(), datetime(), ?, ?)",
  [listId, "Buy groceries"]
);

// Update a todo
await db.Execute(
  "UPDATE todos SET completed = 1, completed_at = datetime() WHERE id = ?",
  [todoId]
);

// Delete a todo
await db.Execute("DELETE FROM todos WHERE id = ?", [todoId]);
use rusqlite::params;

// Insert a new todo
async fn insert_todo(
    db: &PowerSyncDatabase,
    list_id: &str,
    description: &str,
) -> Result<(), PowerSyncError> {
    let writer = db.writer().await?;
    writer.execute(
        "INSERT INTO todos (id, created_at, list_id, description) VALUES (uuid(), date(), ?, ?)",
        params![list_id, description],
    )?;
    Ok(())
}

// Update a todo
async fn complete_todo(db: &PowerSyncDatabase, todo_id: &str) -> Result<(), PowerSyncError> {
    let writer = db.writer().await?;
    writer.execute(
        "UPDATE todos SET completed = 1, completed_at = date() WHERE id = ?",
        params![todo_id],
    )?;
    Ok(())
}

// Delete a todo
async fn delete_todo(db: &PowerSyncDatabase, todo_id: &str) -> Result<(), PowerSyncError> {
    let writer = db.writer().await?;
    writer.execute("DELETE FROM todos WHERE id = ?", params![todo_id])?;
    Ok(())
}
Best practice: Use UUIDs when inserting new rows on the client side. UUIDs can be generated offline/locally, allowing for unique identification of records created in the client database before they are synced to the server. See Client ID for more details.
Learn MoreFor more details, see the Writing Data page.

Next Steps

For production deployments, you’ll need to:
  1. Implement Authentication: Replace development tokens with proper JWT-based authentication. PowerSync supports various authentication providers including Supabase, Firebase Auth, Auth0, Clerk, and custom JWT implementations.
  2. Configure & Integrate Your Backend Application: Set up your backend to handle mutations uploaded from clients.

Additional Resources

Questions?

Try “Ask AI” on this site which is trained on all our documentation, repositories and Discord discussions. Also join us on our community Discord server where you can browse topics from the PowerSync community and chat with our team.