PowerSyncContext.Provider above the component, with your database instance as its value.
Using Transactions to Group Changes
A transaction groups several writes into one unit. Either all of the writes are saved, or none of them are. PowerSync also queues the changes from one transaction for upload as a group. These examples delete a list and its todos together, so the app never keeps todos for a deleted list.- JavaScript Web
- React Native & Expo
- Node.js
- Capacitor
- Dart/Flutter
- Kotlin
- Swift
- .NET
- Rust
writeTransaction() runs your callback inside a transaction. PowerSync saves the changes when the callback finishes, and rolls them back if the callback throws an error.async function deleteList(db, listId) {
await db.writeTransaction(async (tx) => {
await tx.execute('DELETE FROM lists WHERE id = ?', [listId]);
await tx.execute('DELETE FROM todos WHERE list_id = ?', [listId]);
});
}
writeTransaction() runs your callback inside a transaction. PowerSync saves the changes when the callback finishes, and rolls them back if the callback throws an error.async function deleteList(db, listId) {
await db.writeTransaction(async (tx) => {
await tx.execute('DELETE FROM lists WHERE id = ?', [listId]);
await tx.execute('DELETE FROM todos WHERE list_id = ?', [listId]);
});
}
writeTransaction() runs your callback inside a transaction. PowerSync saves the changes when the callback finishes, and rolls them back if the callback throws an error.async function deleteList(db, listId) {
await db.writeTransaction(async (tx) => {
await tx.execute('DELETE FROM lists WHERE id = ?', [listId]);
await tx.execute('DELETE FROM todos WHERE list_id = ?', [listId]);
});
}
The Capacitor SDK uses the same API as the JavaScript Web SDK, so the JavaScript Web example also applies.
writeTransaction() runs your callback inside a transaction. PowerSync saves the changes when the callback finishes, and rolls them back if the callback throws.Future<void> deleteList(PowerSyncDatabase db, String id) async {
await db.writeTransaction((tx) async {
await tx.execute('DELETE FROM lists WHERE id = ?', [id]);
await tx.execute('DELETE FROM todos WHERE list_id = ?', [id]);
});
}
writeTransactionAsync runs the block inside a transaction. Run each statement with executeAsync() on the transaction that the block receives. PowerSync saves the changes when the block finishes, and rolls them back if the block throws an exception.suspend fun deleteList(database: PowerSyncDatabase, listId: String) {
database.writeTransactionAsync { transaction ->
transaction.executeAsync(
sql = "DELETE FROM lists WHERE id = ?",
parameters = listOf(listId)
)
transaction.executeAsync(
sql = "DELETE FROM todos WHERE list_id = ?",
parameters = listOf(listId)
)
}
}
writeTransaction runs the closure inside a transaction. Statements inside the closure run synchronously, so they use try without await. PowerSync saves the changes when the closure finishes, and rolls them back if the closure throws.func deleteList(db: any PowerSyncDatabaseProtocol, listId: String) async throws {
try await db.writeTransaction { tx in
try tx.execute(sql: "DELETE FROM lists WHERE id = ?", parameters: [listId])
try tx.execute(sql: "DELETE FROM todos WHERE list_id = ?", parameters: [listId])
}
}
WriteTransaction runs the callback inside a transaction. PowerSync saves the changes when the callback finishes, and rolls them back if the callback throws an exception.using PowerSync.Common.Client;
class Demo
{
static async Task DeleteList(PowerSyncDatabase db, string listId)
{
await db.WriteTransaction(async (tx) =>
{
await tx.Execute("DELETE FROM lists WHERE id = ?", new object[] { listId });
await tx.Execute("DELETE FROM todos WHERE list_id = ?", new object[] { listId });
});
}
}
db.writer() returns the write connection, and transaction() starts a transaction on it. Call commit() to save the changes. If the function returns early with an error, the transaction is dropped and its changes are rolled back.use powersync::{PowerSyncDatabase, error::PowerSyncError};
use rusqlite::params;
async fn delete_list(db: &PowerSyncDatabase, id: &str) -> Result<(), PowerSyncError> {
let mut writer = db.writer().await?;
let tx = writer.transaction()?;
tx.execute("DELETE FROM lists WHERE id = ?", params![id])?;
tx.execute("DELETE FROM todos WHERE list_id = ?", params![id])?;
tx.commit()?;
Ok(())
}
Listen for Changes in Data
A watch query runs a SQL query again whenever the tables it reads from change, and gives you the new results. Use it to keep your UI up to date as data changes.- JavaScript Web
- React Native & Expo
- Node.js
- Capacitor
- Dart/Flutter
- Kotlin
- Swift
- .NET
- Rust
watch() gives you new results each time the query’s tables change. You can read the results with an async iterator (for await), or receive them in an onResult callback.- AsyncIterator approach
- Callback approach
async function* pendingLists() {
for await (const result of db.watch(
`SELECT * FROM lists WHERE state = ?`,
['pending']
)) {
yield result.rows?._array ?? [];
}
}
const pendingLists = (onResult) => {
db.watch(
'SELECT * FROM lists WHERE state = ?',
['pending'],
{
onResult: (result) => {
onResult(result.rows?._array ?? []);
}
}
);
}
- Live Queries / Watch Queries, including incremental and differential results
watch()API reference
watch() gives you new results each time the query’s tables change. You can read the results with an async iterator (for await), or receive them in an onResult callback.- AsyncIterator approach
- Callback approach
async function* pendingLists() {
for await (const result of db.watch(
`SELECT * FROM lists WHERE state = ?`,
['pending']
)) {
yield result.rows?._array ?? [];
}
}
const pendingLists = (onResult) => {
db.watch(
'SELECT * FROM lists WHERE state = ?',
['pending'],
{
onResult: (result) => {
onResult(result.rows?._array ?? []);
}
}
);
}
- Live Queries / Watch Queries, including incremental and differential results
watch()API reference
watch() gives you new results each time the query’s tables change. You can read the results with an async iterator (for await), or receive them in an onResult callback.- AsyncIterator approach
- Callback approach
async function* pendingLists() {
for await (const result of db.watch(
`SELECT * FROM lists WHERE state = ?`,
['pending']
)) {
yield result.rows?._array ?? [];
}
}
const pendingLists = (onResult) => {
db.watch(
'SELECT * FROM lists WHERE state = ?',
['pending'],
{
onResult: (result) => {
onResult(result.rows?._array ?? []);
}
}
);
}
- Live Queries / Watch Queries, including incremental and differential results
watch()API reference
The Capacitor SDK uses the same API as the JavaScript Web SDK, so the JavaScript Web example also applies.
watch() returns a Stream that emits new results each time the query’s tables change. This example uses a StreamBuilder to rebuild the widget with each result.import 'package:flutter/material.dart';
import 'package:powersync/powersync.dart';
class PendingLists extends StatelessWidget {
final PowerSyncDatabase db;
const PendingLists({super.key, required this.db});
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: db.watch('SELECT id, name FROM lists WHERE state = ?', ['pending']),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Text('Could not load lists.');
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final lists = snapshot.requireData;
return ListView(
children: [
for (final list in lists)
ListTile(
key: ValueKey(list['id']),
title: Text(list['name'] as String),
),
],
);
},
);
}
}
watch() returns a Flow that emits new results each time the query’s tables change. The mapper function converts each row into a ListItem.fun watchPendingLists(): Flow<List<ListItem>> =
db.watch(
"SELECT * FROM lists WHERE state = ?",
listOf("pending"),
) { cursor ->
ListItem(
id = cursor.getString("id"),
name = cursor.getString("name"),
)
}
watch() returns an AsyncThrowingStream that emits new results each time the query’s tables change. The mapper closure converts each row into a ListContent.func watchPendingLists() throws -> AsyncThrowingStream<[ListContent], Error> {
try db.watch(
sql: "SELECT * FROM lists WHERE state = ?",
parameters: ["pending"],
) { cursor in
try ListContent(
id: cursor.getString(name: "id"),
name: cursor.getString(name: "name"),
)
}
}
Watch() returns an IAsyncEnumerable (since v0.0.11-alpha.1) that yields new results each time the query’s tables change. Cancel the token to stop watching.// Define a result type with properties matching the schema columns (some columns omitted here for brevity)
// public class ListResult { public string id; public string name; public string owner_id; ... }
// Optional cancellation token to stop watching
var cts = new CancellationTokenSource();
// Register listener synchronously on the calling thread...
var listener = db.Watch<ListResult>(
"SELECT * FROM lists WHERE owner_id = ?",
[ownerId],
new SQLWatchOptions { Signal = cts.Token }
);
// ...then listen to changes on another thread (or await foreach directly if already in an async context)
_ = Task.Run(async () =>
{
await foreach (var results in listener)
{
Console.WriteLine("Lists: ");
foreach (var result in results)
{
Console.WriteLine($"{result.id}: {result.name}");
}
}
}, cts.Token);
// To stop watching, cancel the token: cts.Cancel();
watch_statement() returns a stream that emits new results each time the query’s tables change. The closure reads the rows of each result.use std::pin::pin;
use futures_lite::StreamExt;
use powersync::{PowerSyncDatabase, error::PowerSyncError};
use rusqlite::params;
struct PendingList {
id: String,
name: String,
}
async fn watch_pending_lists(db: &PowerSyncDatabase) -> Result<(), PowerSyncError> {
let stream = db.watch_statement(
"SELECT id, name FROM lists WHERE state = ?".to_string(),
params!["pending"],
|stmt, params| {
let mut rows = stmt.query(params)?;
let mut lists = vec![];
while let Some(row) = rows.next()? {
lists.push(PendingList {
id: row.get("id")?,
name: row.get("name")?,
});
}
Ok(lists)
},
);
let mut stream = pin!(stream);
// Run this function in a separate async task because the stream keeps listening.
while let Some(lists) = stream.try_next().await? {
for list in lists {
println!("{}: {}", list.id, list.name);
}
}
Ok(())
}
Insert, Update, and Delete Data in the Local Database
Write to the local SQLite database with standard SQLINSERT, UPDATE, and DELETE statements. PowerSync applies each write locally right away and queues it for upload to your backend.
The insert examples use uuid(), a SQL function that PowerSync provides, to generate an ID for each new row.
- JavaScript Web
- React Native & Expo
- Node.js
- Capacitor
- Dart/Flutter
- Kotlin
- Swift
- .NET
- Rust
execute() runs a SQL statement. The ? placeholders take their values from the parameter array. This React button inserts a new customer when clicked.export function AddCustomerButton({ db }) {
const handleButtonClick = async () => {
await db.execute(
'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)',
['Fred', 'fred@example.org']
);
};
return <button onClick={handleButtonClick}>Add customer</button>;
}
execute() runs a SQL statement. The ? placeholders take their values from the parameter array. This React Native button inserts a new customer when pressed.import { Button } from 'react-native';
export function AddCustomerButton({ db }) {
const handleButtonClick = async () => {
await db.execute(
'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)',
['Fred', 'fred@example.org']
);
};
return <Button title="Add customer" onPress={handleButtonClick} />;
}
execute() runs a SQL statement. The ? placeholders take their values from the parameter array.await db.execute(
'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)',
['Fred', 'fred@example.org']
);
The Capacitor SDK uses the same API as the JavaScript Web SDK, so the JavaScript Web example also applies.
execute() runs a SQL statement. The ? placeholders take their values from the parameter list. This button inserts a new customer when tapped.FloatingActionButton(
onPressed: () async {
await db.execute(
'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)',
['Fred', 'fred@example.org'],
);
},
tooltip: 'Add customer',
child: const Icon(Icons.add),
);
execute() runs a SQL statement. The ? placeholders take their values from the parameter list. This function updates a customer’s name and email.suspend fun updateCustomer(id: String, name: String, email: String) {
database.execute(
"UPDATE customers SET name = ?, email = ? WHERE id = ?",
listOf(name, email, id)
)
}
execute() runs a SQL statement. The ? placeholders take their values from the parameters array. This function inserts a new customer.func insertCustomer(name: String, email: String) async throws {
try await db.execute(
sql: "INSERT INTO customers (id, name, email) VALUES (uuid(), ?, ?)",
parameters: [name, email]
)
}
Execute() runs a SQL statement. The ? placeholders take their values from the parameter array.await db.Execute(
"INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)",
new object[] { "Fred", "fred@example.org" }
);
db.writer() returns the write connection. Call execute() on it with the SQL statement and its parameters.use powersync::{PowerSyncDatabase, error::PowerSyncError};
use rusqlite::params;
async fn insert_customer(
db: &PowerSyncDatabase,
name: &str,
email: &str,
) -> Result<(), PowerSyncError> {
let writer = db.writer().await?;
writer.execute(
"INSERT INTO customers (id, name, email) VALUES (uuid(), ?, ?)",
params![name, email],
)?;
Ok(())
}
Send Changes in Local Data to Your Backend Service
PowerSync does not write to your source database directly. While the app is connected, the Client SDK calls theuploadData() method of your backend connector whenever local changes are waiting in the upload queue. Your implementation sends these changes to your backend API, which applies them to your source database.
The examples below take the oldest queued transaction, send all of its changes to your backend, and then mark the transaction as complete. Each change includes the operation (PUT, PATCH, or DELETE), the table, the row ID, and the changed data. backendApi stands for your own API client. If the upload fails, throw an error. The SDK then keeps the changes in the queue and retries later.
To generate a complete backend connector for your SDK, open your instance in the PowerSync Dashboard, select Client SDK Setup, and select your app language.
- JavaScript Web
- React Native & Expo
- Node.js
- Capacitor
- Dart/Flutter
- Kotlin
- Swift
- .NET
- Rust
getNextCrudTransaction() returns the oldest transaction in the upload queue, or null if the queue is empty. Call complete() only after your backend has accepted the changes.async function uploadData(database) {
const transaction = await database.getNextCrudTransaction();
if (!transaction) return;
await backendApi.uploadChanges(transaction.crud);
await transaction.complete();
}
getNextCrudTransaction() returns the oldest transaction in the upload queue, or null if the queue is empty. Call complete() only after your backend has accepted the changes.async function uploadData(database) {
const transaction = await database.getNextCrudTransaction();
if (!transaction) return;
await backendApi.uploadChanges(transaction.crud);
await transaction.complete();
}
getNextCrudTransaction() returns the oldest transaction in the upload queue, or null if the queue is empty. Call complete() only after your backend has accepted the changes.async function uploadData(database) {
const transaction = await database.getNextCrudTransaction();
if (!transaction) return;
await backendApi.uploadChanges(transaction.crud);
await transaction.complete();
}
getNextCrudTransaction() returns the oldest transaction in the upload queue, or null if the queue is empty. Call complete() only after your backend has accepted the changes.async function uploadData(database) {
const transaction = await database.getNextCrudTransaction();
if (!transaction) return;
await backendApi.uploadChanges(transaction.crud);
await transaction.complete();
}
getNextCrudTransaction() returns the oldest transaction in the upload queue, or null if the queue is empty. Call complete() only after your backend has accepted the changes.@override
Future<void> uploadData(PowerSyncDatabase database) async {
final transaction = await database.getNextCrudTransaction();
if (transaction == null) return;
await backendApi.uploadChanges(transaction.crud);
await transaction.complete();
}
getNextCrudTransaction() returns the oldest transaction in the upload queue, or null if the queue is empty. Call complete() only after your backend has accepted the changes. Its argument is an optional write checkpoint that most apps don’t use, so pass null.override suspend fun uploadData(database: PowerSyncDatabase) {
val transaction = database.getNextCrudTransaction() ?: return
backendApi.uploadChanges(transaction.crud)
transaction.complete(null)
}
getNextCrudTransaction() returns the oldest transaction in the upload queue, or nil if the queue is empty. Call complete() only after your backend has accepted the changes.func uploadData(database: any PowerSyncDatabaseProtocol) async throws {
guard let transaction = try await database.getNextCrudTransaction() else { return }
try await backendApi.uploadChanges(transaction.crud)
try await transaction.complete()
}
GetNextCrudTransaction() returns the oldest transaction in the upload queue, or null if the queue is empty. Call Complete() only after your backend has accepted the changes.public async Task UploadData(IPowerSyncDatabase database)
{
var transaction = await database.GetNextCrudTransaction();
if (transaction == null) return;
await backendApi.UploadChanges(transaction.Crud);
await transaction.Complete();
}
crud_transactions() returns a stream of the queued transactions, oldest first. This example uploads each transaction and completes it before it moves to the next one.use futures_lite::StreamExt;
// Inside your BackendConnector implementation
async fn upload_data(&self) -> Result<(), PowerSyncError> {
let mut transactions = self.db.crud_transactions();
while let Some(transaction) = transactions.try_next().await? {
self.backend_api.upload_changes(&transaction.crud).await?;
transaction.complete().await?;
}
Ok(())
}
Accessing PowerSync Connection Status Information
The sync status tells you whether the app is connected to the PowerSync Service, and whether data is uploading or downloading. Use it to show a connection indicator in your UI.- JavaScript Web
- React Native & Expo
- Node.js
- Capacitor
- Dart/Flutter
- Kotlin
- Swift
- .NET
- Rust
useStatus() returns the current sync status and re-renders the component when it changes. Read connected to show the connection state.import { useStatus } from '@powersync/react';
export function ConnectionStatus() {
const { connected } = useStatus();
return <span>{connected ? 'Connected' : 'Disconnected'}</span>;
}
useStatus() returns the current sync status and re-renders the component when it changes. Read connected to show the connection state.import { useStatus } from '@powersync/react-native';
import { Text } from 'react-native';
export function ConnectionStatus() {
const { connected } = useStatus();
return <Text>{connected ? 'Connected' : 'Disconnected'}</Text>;
}
registerListener() calls statusChanged whenever the status changes, and returns a function that removes the listener.const removeListener = db.registerListener({
statusChanged: (status) => {
console.log(status.connected ? 'Connected' : 'Disconnected');
}
});
// Call removeListener() to stop listening
The Capacitor SDK uses the same API as the JavaScript Web SDK, so the JavaScript Web example also applies.
statusStream emits a new SyncStatus whenever the status changes, and currentStatus holds the latest one. This widget uses a StreamBuilder to update an icon for the current state, including errors, connecting, uploading, and downloading.import 'package:flutter/material.dart';
import 'package:powersync/powersync.dart';
class ConnectionStatus extends StatelessWidget {
final PowerSyncDatabase db;
const ConnectionStatus({super.key, required this.db});
@override
Widget build(BuildContext context) {
return StreamBuilder<SyncStatus>(
stream: db.statusStream,
initialData: db.currentStatus,
builder: (context, snapshot) => _getStatusIcon(snapshot.requireData),
);
}
}
Widget _makeIcon(String label, IconData icon) {
return Tooltip(message: label, child: Icon(icon));
}
Widget _getStatusIcon(SyncStatus status) {
if (status.anyError != null) {
if (!status.connected) {
return _makeIcon(status.anyError!.toString(), Icons.cloud_off);
} else {
return _makeIcon(status.anyError!.toString(), Icons.sync_problem);
}
} else if (status.connecting) {
return _makeIcon('Connecting', Icons.cloud_sync_outlined);
} else if (!status.connected) {
return _makeIcon('Not connected', Icons.cloud_off);
} else if (status.uploading && status.downloading) {
return _makeIcon('Uploading and downloading', Icons.cloud_sync_outlined);
} else if (status.uploading) {
return _makeIcon('Uploading', Icons.cloud_sync_outlined);
} else if (status.downloading) {
return _makeIcon('Downloading', Icons.cloud_sync_outlined);
} else {
return _makeIcon('Connected', Icons.cloud_queue);
}
}
currentStatus holds the sync status. In Compose, composeState() turns it into state that updates the UI whenever the status changes. composeState() comes from the com.powersync:powersync-compose artifact.@Composable
fun ConnectionStatus(db: PowerSyncDatabase) {
val status by db.currentStatus.composeState()
Text(if (status.connected) "Connected" else "Disconnected")
}
currentStatus.observable returns a status object that SwiftUI observes. The view updates whenever the status changes. This requires iOS 17, macOS 14, or later.import SwiftUI
import PowerSync
struct PowerSyncConnectionIndicator: View {
private let status: ObservableSyncStatus
init(powersync: any PowerSyncDatabaseProtocol) {
self.status = powersync.currentStatus.observable
}
var body: some View {
let connected = status.connected
let iconName = connected ? "wifi" : "wifi.slash"
let description = connected ? "Online" : "Offline"
Image(systemName: iconName)
.accessibility(label: Text(description))
}
}
Events.OnStatusChanged.ListenAsync() (since v0.0.11-alpha.1) yields an update each time the status changes. Pass a cancellation token to stop listening.using PowerSync.Common.Client;
async Task ObserveStatus(PowerSyncDatabase db, CancellationToken ct = default)
{
var listener = db.Events.OnStatusChanged.ListenAsync(ct);
await foreach (var update in listener)
{
var status = update.Status;
var dataFlow = status.DataFlowStatus;
Console.WriteLine($"Connected: {status.Connected}");
Console.WriteLine($"Uploading: {dataFlow.Uploading}, downloading: {dataFlow.Downloading}");
}
}
watch_status() returns a stream of status updates. The first item is the current status. To read the current status without waiting, call db.status().use futures_lite::StreamExt;
use powersync::PowerSyncDatabase;
async fn observe_status(db: &PowerSyncDatabase) {
let mut statuses = db.watch_status();
while let Some(status) = statuses.next().await {
println!("Connected: {}", status.is_connected());
}
}
Wait for the Initial Sync to Complete
When a user first opens your app, the local database has no synced data until the first sync finishes. PowerSync gives you two ways to know when that first full sync has completed:hasSyncedis a property of the sync status. It becomestrueonce the first full sync has completed. Use it in UI code that updates when the status changes, for example to show a loading screen instead of an empty list.waitForFirstSync()waits until the first full sync has completed, then returns. Use it in code that needs the synced data before it continues, for example before a query that runs at startup.
hasSynced is already true and waitForFirstSync() returns right away.
- JavaScript Web
- React Native & Expo
- Node.js
- Capacitor
- Dart/Flutter
- Kotlin
- Swift
- .NET
- Rust
hasSynced is true once the first full sync has completed. useStatus() updates the component when that happens. To wait in code instead, call await db.waitForFirstSync().import { useStatus } from '@powersync/react';
export function InitialSyncStatus() {
const { hasSynced } = useStatus();
return <div>{hasSynced ? 'Initial sync completed!' : 'Busy with initial sync...'}</div>;
}
hasSynced is true once the first full sync has completed. useStatus() updates the component when that happens. To wait in code instead, call await db.waitForFirstSync().import { useStatus } from '@powersync/react-native';
import { Text } from 'react-native';
export function InitialSyncStatus() {
const { hasSynced } = useStatus();
return <Text>{hasSynced ? 'Initial sync completed!' : 'Busy with initial sync...'}</Text>;
}
waitForFirstSync() returns a promise that resolves once the first full sync has completed. To check without waiting, read db.currentStatus.hasSynced.await db.waitForFirstSync();
// The local database now contains the synced data
const lists = await db.getAll('SELECT * FROM lists');
The Capacitor SDK uses the same API as the JavaScript Web SDK, so the JavaScript Web example also applies.
hasSynced is true once the first full sync has completed. This StreamBuilder rebuilds the text whenever the status changes. To wait in code instead, call await db.waitForFirstSync().StreamBuilder<SyncStatus>(
stream: db.statusStream,
initialData: db.currentStatus,
builder: (context, snapshot) {
final hasSynced = snapshot.requireData.hasSynced ?? false;
return Text(hasSynced ? 'Initial sync completed!' : 'Busy with initial sync...');
},
)
hasSynced is true once the first full sync has completed. This composable shows a message until then, and your content afterward. composeState() comes from the com.powersync:powersync-compose artifact. To wait in code instead, call db.waitForFirstSync() from a coroutine.@Composable
fun InitialSyncGate(db: PowerSyncDatabase, content: @Composable () -> Unit) {
val status by db.currentStatus.composeState()
if (status.hasSynced == true) {
content()
} else {
Text("Busy with initial sync...")
}
}
hasSynced is true once the first full sync has completed. This view reads it from currentStatus.observable, which SwiftUI observes, so the view shows a spinner until then and your content afterward. currentStatus.observable requires iOS 17, macOS 14, or later. To wait in code instead, call try await db.waitForFirstSync().import SwiftUI
import PowerSync
struct InitialSyncGate<Content: View>: View {
private let status: ObservableSyncStatus
private let content: Content
init(powersync: any PowerSyncDatabaseProtocol, @ViewBuilder content: () -> Content) {
self.status = powersync.currentStatus.observable
self.content = content()
}
var body: some View {
if status.hasSynced == true {
content
} else {
ProgressView("Busy with initial sync...")
}
}
}
CurrentStatus.HasSynced is true once the first full sync has completed. To wait in code instead, call await db.WaitForFirstSync().if (db.CurrentStatus.HasSynced == true)
{
Console.WriteLine("Initial sync completed!");
}
else
{
Console.WriteLine("Busy with initial sync...");
}
// Wait until the first full sync has completed
await db.WaitForFirstSync();
has_synced() is true once the first full sync has completed. To check without waiting, call db.status().has_synced(). The Rust SDK has no wait_for_first_sync() on the database, so this function waits for the first status update where has_synced() is true.use futures_lite::StreamExt;
use powersync::PowerSyncDatabase;
async fn wait_for_initial_sync(db: &PowerSyncDatabase) {
db.watch_status()
.filter(|status| status.has_synced())
.next()
.await;
}
Wait for Local Data to Catch Up
The first sync may have completed in an earlier app session, sohasSynced and waitForFirstSync() don’t tell you whether local data is up to date now. To wait until the local database has caught up with the server as of now, use Sync Catch-Up: request a checkpoint, then wait for it to sync. Most of these examples use it for a refresh action, so the refresh indicator stops only once the device has caught up.
Checkpoint requests are in alpha. They require PowerSync Service v1.24.0 or later, and you must enable them with the checkpointMode option when calling connect(). See Prerequisites.
- JavaScript Web
- React Native & Expo
- Node.js
- Capacitor
- Dart/Flutter
- Kotlin
- Swift
- .NET
- Rust
requestCheckpoint() asks the PowerSync Service for a checkpoint. waitForSync() resolves once the local database has synced that checkpoint. This React button stays disabled while it waits.import { useState } from 'react';
import { usePowerSync } from '@powersync/react';
export function RefreshButton() {
const powerSync = usePowerSync();
const [refreshing, setRefreshing] = useState(false);
async function refresh() {
setRefreshing(true);
try {
const checkpoint = await powerSync.requestCheckpoint();
await checkpoint.waitForSync();
} catch (e) {
// Show the error to the user
} finally {
setRefreshing(false);
}
}
return (
<button onClick={refresh} disabled={refreshing}>
{refreshing ? 'Refreshing...' : 'Refresh'}
</button>
);
}
requestCheckpoint() asks the PowerSync Service for a checkpoint. waitForSync() resolves once the local database has synced that checkpoint. RefreshControl shows the pull-to-refresh indicator until the wait finishes.import { useState } from 'react';
import { RefreshControl, ScrollView } from 'react-native';
import { usePowerSync } from '@powersync/react-native';
export function ListsScreen() {
const powerSync = usePowerSync();
const [refreshing, setRefreshing] = useState(false);
const onRefresh = async () => {
setRefreshing(true);
try {
const checkpoint = await powerSync.requestCheckpoint();
await checkpoint.waitForSync();
} catch (e) {
// Show the error to the user
} finally {
setRefreshing(false);
}
};
return (
<ScrollView refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}>
{/* List items */}
</ScrollView>
);
}
requestCheckpoint() asks the PowerSync Service for a checkpoint. waitForSync() resolves once the local database has synced that checkpoint.const checkpoint = await db.requestCheckpoint();
await checkpoint.waitForSync();
// This query now includes all server changes up to when you created the request
const lists = await db.getAll('SELECT * FROM lists');
The Capacitor SDK uses the same API as the JavaScript Web SDK, so the JavaScript Web example also applies.
requestCheckpoint() asks the PowerSync Service for a checkpoint. waitForSync() completes once the local database has synced that checkpoint. RefreshIndicator shows its spinner until the onRefresh future completes.RefreshIndicator(
onRefresh: () async {
try {
final checkpoint = await db.requestCheckpoint();
await checkpoint.waitForSync();
} catch (e) {
// Show the error to the user
}
},
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: listItems,
),
)
requestCheckpoint() asks the PowerSync Service for a checkpoint. waitForSync() returns once the local database has synced that checkpoint. Both are suspend functions, so this button calls them from a coroutine. They require opting in to ExperimentalCheckpointRequestsApi.@OptIn(ExperimentalCheckpointRequestsApi::class)
@Composable
fun RefreshButton(database: PowerSyncDatabase) {
val scope = rememberCoroutineScope()
var isRefreshing by remember { mutableStateOf(false) }
IconButton(
onClick = {
scope.launch {
isRefreshing = true
try {
val checkpoint = database.requestCheckpoint()
checkpoint.waitForSync()
} catch (e: Exception) {
if (e is CancellationException) throw e
// Show the error to the user
} finally {
isRefreshing = false
}
}
},
enabled = !isRefreshing,
) {
Icon(Icons.Default.Refresh, contentDescription = "Refresh")
}
}
requestCheckpoint() asks the PowerSync Service for a checkpoint. waitForSync() returns once the local database has synced that checkpoint. .refreshable shows the pull-to-refresh indicator until its closure returns.List(lists) { list in
Text(list.name)
}
.refreshable {
do {
let checkpoint = try await db.requestCheckpoint()
try await checkpoint.waitForSync()
} catch {
// Show the error to the user
}
}
RequestCheckpoint() asks the PowerSync Service for a checkpoint. WaitForSync() completes once the local database has synced that checkpoint. A MAUI RefreshView raises Refreshing when the user pulls to refresh. The handler hides the indicator by setting IsRefreshing to false.// In XAML: <RefreshView x:Name="ListsRefreshView" Refreshing="OnRefreshing">
private async void OnRefreshing(object sender, EventArgs e)
{
try
{
var checkpoint = await db.RequestCheckpoint();
await checkpoint.WaitForSync();
}
catch (Exception ex)
{
await DisplayAlert("Refresh failed", ex.Message, "OK");
}
finally
{
ListsRefreshView.IsRefreshing = false;
}
}
request_checkpoint() asks the PowerSync Service for a checkpoint. wait_for_sync() returns once the local database has synced that checkpoint. Both return a CheckpointError if they fail.use powersync::{CheckpointError, PowerSyncDatabase};
async fn refresh(db: &PowerSyncDatabase) -> Result<(), CheckpointError> {
let checkpoint = db.request_checkpoint().await?;
checkpoint.wait_for_sync().await
}
Report Sync Download Progress
While a download is in progress, the sync status reports how many operations have downloaded out of the total. Use it to show a progress bar, especially during a long first sync. When no download is active, there is no progress value. Several of these examples accept an optional priority. With a priority, they show progress toward the data with that priority and all higher priorities instead of the full sync. See Prioritized Sync.- JavaScript Web
- React Native & Expo
- Node.js
- Capacitor
- Dart/Flutter
- Kotlin
- Swift
- .NET
- Rust
useStatus() returns the current sync status and re-renders the component when it changes. downloadProgress.downloadedFraction is a value from 0.0 to 1.0. This example uses MUI components.import { Box, LinearProgress, Stack, Typography } from '@mui/material';
import { useStatus } from '@powersync/react';
import type { FC } from 'react';
export const SyncProgressBar: FC<{ priority?: number }> = ({ priority }) => {
const status = useStatus();
const progressUntilNextSync = status.downloadProgress;
const progress = priority == null ? progressUntilNextSync : progressUntilNextSync?.untilPriority(priority);
if (progress == null) {
return <></>;
}
return (
<Stack direction="column" spacing={1} sx={{ p: 4 }} alignItems="stretch">
<LinearProgress variant="determinate" value={progress.downloadedFraction * 100} />
<Box sx={{ alignSelf: 'center' }}>
{progress.downloadedOperations == progress.totalOperations ? (
<Typography>Applying server-side changes</Typography>
) : (
<Typography>
Downloaded {progress.downloadedOperations} out of {progress.totalOperations}.
</Typography>
)}
</Box>
</Stack>
);
};
useStatus() returns the current sync status and re-renders the component when it changes. downloadProgress.downloadedFraction is a value from 0.0 to 1.0. This example uses LinearProgress from React Native Elements, which expects a value in that range.import { useStatus } from '@powersync/react';
import type { FC } from 'react';
import { View } from 'react-native';
import { Text, LinearProgress } from '@rneui/themed';
export const SyncProgressBar: FC<{ priority?: number }> = ({ priority }) => {
const status = useStatus();
const progressUntilNextSync = status.downloadProgress;
const progress = priority == null ? progressUntilNextSync : progressUntilNextSync?.untilPriority(priority);
if (progress == null) {
return <></>;
}
return (
<View>
<LinearProgress variant="determinate" value={progress.downloadedFraction} />
{progress.downloadedOperations == progress.totalOperations ? (
<Text>Applying server-side changes</Text>
) : (
<Text>
Downloaded {progress.downloadedOperations} out of {progress.totalOperations}.
</Text>
)}
</View>
);
};
downloadProgress is null when no download is active. downloadedOperations and totalOperations count the operations in the current download.db.registerListener({
statusChanged: (status) => {
const progress = status.downloadProgress;
if (progress) {
console.log(`Downloaded ${progress.downloadedOperations} of ${progress.totalOperations} operations`);
}
}
});
The Capacitor SDK uses the same API as the JavaScript Web SDK, so the JavaScript Web example also applies.
downloadProgress is null when no download is active. downloadedFraction is a value from 0.0 to 1.0. This widget shows a progress bar while a download is active.import 'package:flutter/material.dart';
import 'package:powersync/powersync.dart' hide Column;
class SyncProgressBar extends StatelessWidget {
final PowerSyncDatabase db;
/// When set, show progress for this priority and all higher priorities.
final StreamPriority? priority;
const SyncProgressBar({
super.key,
required this.db,
this.priority,
});
@override
Widget build(BuildContext context) {
return StreamBuilder<SyncStatus>(
stream: db.statusStream,
initialData: db.currentStatus,
builder: (context, snapshot) {
final status = snapshot.requireData;
final progress = switch (priority) {
null => status.downloadProgress,
var priority? => status.downloadProgress?.untilPriority(priority),
};
if (progress != null) {
return Center(
child: Column(
children: [
const Text('Busy with sync...'),
LinearProgressIndicator(value: progress.downloadedFraction),
Text(
'${progress.downloadedOperations} out of ${progress.totalOperations}')
],
),
);
} else {
return const SizedBox.shrink();
}
},
);
}
}
downloadProgress is null when no download is active. fraction is a value from 0.0 to 1.0. composeState() comes from the com.powersync:powersync-compose artifact.import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.powersync.PowerSyncDatabase
import com.powersync.bucket.StreamPriority
import com.powersync.compose.composeState
/**
* Shows a progress bar while a sync is active.
*
* When [priority] is set, include that priority and all higher priorities.
*/
@Composable
fun SyncProgressBar(
db: PowerSyncDatabase,
priority: StreamPriority? = null,
) {
val state by db.currentStatus.composeState()
val progress = state.downloadProgress?.let {
if (priority == null) {
it
} else {
it.untilPriority(priority)
}
}
if (progress == null) {
return
}
Column(
modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth().padding(8.dp),
progress = progress.fraction,
)
if (progress.downloadedOperations == progress.totalOperations) {
Text("Applying server-side changes...")
} else {
Text("Downloaded ${progress.downloadedOperations} out of ${progress.totalOperations}.")
}
}
}
downloadProgress is nil when no download is active. fraction is a value from 0.0 to 1.0. currentStatus.observable requires iOS 17, macOS 14, or later.struct SyncProgressIndicator: View {
private let status: ObservableSyncStatus
private let priority: BucketPriority?
init(powersync: any PowerSyncDatabaseProtocol, priority: BucketPriority? = nil) {
self.status = powersync.currentStatus.observable
self.priority = priority
}
var body: some View {
VStack {
if let totalProgress = status.downloadProgress {
let progress = if let priority = self.priority {
totalProgress.untilPriority(priority: priority)
} else {
totalProgress
}
ProgressView(value: progress.fraction)
if progress.downloadedOperations == progress.totalOperations {
Text("Applying server-side changes...")
} else {
Text("Downloaded \(progress.downloadedOperations) out of \(progress.totalOperations)")
}
}
}
}
}
DownloadProgress() returns null when no download is active. DownloadedFraction is a value from 0.0 to 1.0. Pass a cancellation token to stop listening.using PowerSync.Common.Client;
async Task ReportSyncProgress(
PowerSyncDatabase db,
int? priority = null,
CancellationToken ct = default)
{
var listener = db.Events.OnStatusChanged.ListenAsync(ct);
await foreach (var update in listener)
{
var totalProgress = update.Status.DownloadProgress();
var progress = priority == null
? totalProgress
: totalProgress?.UntilPriority(priority.Value);
if (progress == null)
{
continue;
}
Console.WriteLine($"Sync progress: {progress.DownloadedFraction:P1}");
if (progress.DownloadedOperations == progress.TotalOperations)
{
Console.WriteLine("Applying server-side changes...");
}
else
{
Console.WriteLine($"Downloaded {progress.DownloadedOperations} out of {progress.TotalOperations} operations");
}
}
}
The Rust SDK doesn’t report progress for the full sync, but it reports progress for each Sync Stream. This example prints the progress of an auto-subscribed Sync Stream named Learn more:
lists.use futures_lite::StreamExt;
use powersync::PowerSyncDatabase;
async fn print_progress(db: &PowerSyncDatabase) {
let stream = db.sync_stream("lists", None);
let mut status = db.watch_status();
while let Some(status) = status.next().await {
let progress = status
.for_stream(&stream)
.and_then(|stream| stream.progress);
if let Some(progress) = progress {
println!(
"Stream progress: {} out of {}",
progress.downloaded, progress.total
);
}
}
}