# Architecture Overview Source: https://docs.powersync.com/architecture/architecture-overview The core components of PowerSync are the service and client SDKs The [PowerSync Service](/architecture/powersync-service) and client SDK operate in unison to keep client-side SQLite databases in sync with a backend database. Learn about their architecture: ### Protocol Learn about the sync protocol used between PowerSync clients and a [PowerSync Service](/architecture/powersync-service): ### Self-Hosted Architecture For more details on typical architecture of a production self-hosted deployment, see here: # Client Architecture Source: https://docs.powersync.com/architecture/client-architecture ### Reading and Writing Data From the client-side perspective, there are two data flow paths: * Reading data from the server or downloading data (to the SQLite database) * Writing changes back to the server, or uploading data (from the SQLite database) #### Reading Data App clients always read data from a local SQLite database. The local database is asynchronously hydrated from the PowerSync Service. A developer configures [Sync Rules](/usage/sync-rules) for their PowerSync instance to control which data is synced to which users. The PowerSync Service connects directly to the backend database and uses a change stream to hydrate dynamic data partitions, called [sync buckets](/usage/sync-rules/organize-data-into-buckets). Sync buckets are used to partition data according to the configured Sync Rules. (In most use cases, only a subset of data is required in a client's database and not a copy of the entire backend database.) The local SQLite database embedded in the PowerSync SDK is automatically kept in sync with the backend database, based on the [Sync Rules](/usage/sync-rules) configured by the developer: #### Writing Data Client-side data modifications, namely updates, deletes and inserts, are persisted in the embedded SQLite database as well as stored in an upload queue. The upload queue is a blocking [FIFO](https://en.wikipedia.org/wiki/FIFO_%28computing_and_electronics%29) queue that gets processed when network connectivity is available. Each entry in the queue is processed by writing the entry to your existing backend application API, using a function [defined by you](/installation/client-side-setup/integrating-with-your-backend) (the developer). This is to ensure that existing backend business logic is honored when uploading data changes. For more information, see the section on [integrating with your backend](/installation/client-side-setup/integrating-with-your-backend). ### Schema On the client, the application [defines a schema](/installation/client-side-setup/define-your-schema) with tables, columns and indexes. These are then usable as if they were actual SQLite tables, while in reality these are created as SQLite views. The client SDK maintains the following tables: 1. `ps_data__` This contains the data for `
` , in JSON format. This table's schema does not change when columns are added, removed or changed. 2. `ps_data_local__
` Same as the above, but for local-only tables. 3. `
` (VIEW) - this is a view on the above table, with each defined column extracted from the JSON field. For example, a "description" text column would be `CAST(data ->> '$.description' as TEXT)`. 4. `ps_untyped` - Any synced table that does is not defined in the client-side schema is placed here. If the table is added to the schema at a later point, the data is then migrated to `ps_data__
`. 5. `ps_oplog` - This is data as received by the [PowerSync Service](/architecture/powersync-service), grouped per bucket. 6. `ps_crud` - The local upload queue. 7. `ps_buckets` - A small amount of metadata for each bucket. 8. `ps_migrations` - Table keeping track of SDK schema migrations. Most rows will be present in at least two tables — the `ps_data__
` table, and in `ps_oplog`. It may be present multiple times in `ps_oplog`, if it was synced via multiple buckets. The copy in `ps_oplog` may be newer than the one in `ps_data__
`. Only when a full checkpoint has been downloaded, will the data be copied over to the individual tables. If multiple rows with the same table and id has been synced, only one will be preserved (the one with the highest `op_id`). If you run into limitations with the above JSON-based SQLite view system, check out [the Raw Tables experimental feature](/usage/use-case-examples/raw-tables) which allows you to define and manage raw SQLite tables to work around some limitations. We are actively seeking feedback about this functionality. # Consistency Source: https://docs.powersync.com/architecture/consistency PowerSync uses the concept of "checkpoints" to ensure the data is consistent. ## PowerSync: Designed for causal+ consistency PowerSync is designed to have [Causal+ Consistency](https://jepsen.io/consistency/models/causal), while providing enough flexibility for applications to perform their own data validations and conflict handling. ## How it works: Checkpoints A checkpoint is a single point-in-time on the server (similar to an [LSN in Postgres](https://www.postgresql.org/docs/current/datatype-pg-lsn.html)) with a consistent state — only fully committed transactions are part of the state. The client only updates its local state when it has all the data matching a checkpoint, and then it updates the state to exactly match that of the checkpoint. There is no intermediate state while downloading large sets of changes such as large server-side transactions. Different tables and sync buckets are all included in the same consistent checkpoint, to ensure that the state is consistent over all data in the app. ## Local client changes Local changes are applied on top of the last checkpoint received from the server, as well as being persisted into an upload queue. While changes are present in the upload queue, the client does not advance to a new checkpoint. This means the client never has to resolve conflicts locally. Only once all the local changes have been acknowledged by the server, and the data for that new checkpoint is downloaded by the client, does the client advance to the next checkpoint. This ensures that the operations are always ordered correctly on the client. ## Types of local operations The client automatically records changes to the local database as PUT, PATCH or DELETE operations — corresponding to INSERT, UPDATE or DELETE statements. These are grouped together in a batch per local transaction. Since the developer has full control over how operations are applied, more advanced operations can be modeled on top of these three. For example an insert-only "operations" table can be added, that records additional metadata for individual operations. ## Validation and conflict handling With PowerSync offering full flexibility in how changes are applied on the server, it is also the developer's responsibility to implement this correctly to avoid consistency issues. Some scenarios to consider: While the client was offline, a record was modified locally. By the time the client is online again, that record has been deleted. Some options for handling the change: * Discard the change. * Discard the entire transaction. * Re-create the record. * Record the change elsewhere, potentially notifying the user and allowing the user to resolve the issue. Some other examples include foreign-key or not-null constraints, maximum size of numeric fields, unique constraints, and access restrictions (such as row-level security policies). With an online-only application, the user typically sees the error as soon as it occurs, and can make changes as required. In an offline-capable application, these errors may occur much later than when the change was made, so more care is required to handle these cases. Special care must be taken so that issues such as those do not block the upload queue — the queue cannot advance if the server does not acknowledge a change. There is no single correct choice on how to handle write failures such as mentioned above — the best action depends on the specific application and scenario. However, we do have some suggestions for general approaches: 1. In general, consider relaxing constraints somewhat on the server where it is not absolutely important. It may be better to accept data that is somewhat inconsistent (e.g. a client not applying all expected validations), rather than discarding the data completely. 2. If it is critical to preserve all client changes and preserve the order of changes: 1. Block the client's queue on unexpected errors (don't acknowledge the change). 2. Implement error monitoring to be notified of issues, and resolve the issues as soon as possible. 3. If it is critical to preserve all client changes, but the exact order may not be critical: 1. On a constraint error, persist the transaction in a separate server-side queue, and acknowledge the change. 2. The server-side queue can then be inspected and retried asynchronously, without blocking the client-side queue. 4. If it is acceptable to lose some changes due to constraint errors: 1. Discard the change, or the entire transaction if the changes must all be applied together. 2. Implement error notifications to detect these issues. See also: * [Handling Update Conflicts](/usage/lifecycle-maintenance/handling-update-conflicts) * [Custom Conflict Resolution](/usage/lifecycle-maintenance/handling-update-conflicts/custom-conflict-resolution) If you have any questions about consistency, please [join our Discord](https://discord.gg/powersync) to discuss. # PowerSync Protocol Source: https://docs.powersync.com/architecture/powersync-protocol This contains a broad overview of the sync protocol used between PowerSync clients and a [PowerSync Service](/architecture/powersync-service) instance. For details, see the implementation in the various client SDKs. ## Design The PowerSync protocol is designed to efficiently sync changes to clients, while maintaining [consistency](/architecture/consistency) and integrity of data. The same process is used to download the initial set of data, bulk download changes after being offline for a while, and incrementally stream changes while connected. ## Concepts ### Buckets All synced data is grouped into [buckets](/usage/sync-rules/organize-data-into-buckets). A bucket represents a collection of synced rows, synced to any number of users. [Buckets](/usage/sync-rules/organize-data-into-buckets) is a core concept that allows PowerSync to efficiently scale to thousands of concurrent users, incrementally syncing changes to hundreds of thousands of rows to each. Each bucket keeps an ordered list of changes to rows within the bucket — generally as "PUT" or "REMOVE" operations. * PUT is the equivalent of "INSERT OR REPLACE" * REMOVE is slightly different from "DELETE": a row is only deleted from the client if it has been removed from *all* buckets synced to the client. ### Checkpoints A checkpoint is a sequential id that represents a single point-in-time for consistency purposes. This is further explained in [Consistency](/architecture/consistency). ### Checksums For any checkpoint, the client and server can compute a per-bucket checksum. This is essentially the sum of checksums of individual operations within the bucket, which each individual checksum being a hash of the operation data. The checksum helps to ensure that the client has all the correct data. If the bucket data changes on the server, for example because of a manual edit to the underlying bucket storage, the checksums will stop matching, and the client will re-download the entire bucket. Note: Checksums are not a cryptographically secure method to verify data integrity. Rather, it is designed to detect simple data mismatches, whether due to bugs, manual data modification, or other corruption issues. ### Compacting To avoid indefinite growth in size of buckets, the history of a bucket can be compacted. Stale updates are replaced with marker entries, which can be merged together, while keeping the same checksums. ## Protocol A client initiates a sync session using: 1. A JWT token that typically contains the user\_id, and additional parameters (optional). 2. A list of current buckets and the latest operation id in each. The server then responds with a stream of: 1. "Checkpoint available": A new checkpoint id, with a checksum for each bucket in the checkpoint. 2. "Data": New operations for the above checkpoint for each relevant bucket, starting from the last operation id as sent by the client. 3. "Checkpoint complete": Sent once all data for the checkpoint have been sent. The server then waits until a new checkpoint is available, then repeats the above sequence. The stream can be interrupted at any time, at which point the client will initiate a new session, resuming from the last point. If a checksum validation fails on the client, the client will delete the bucket and start a new sync session. Data for individual rows are represented using JSON. The protocol itself is schemaless - the client is expected to use their own copy of the schema, and gracefully handle schema differences. #### Write Checkpoints Write checkpoints are used to ensure clients have synced their own changes back before applying downloaded data locally. Creating a write checkpoint is a separate operation, which is performed by the client after all data has been uploaded. It is important that this happens after the data has been written to the backend source database. The server then keeps track of the current CDC stream position on the database (LSN in Postgres and SQL Server, resume token in MongoDB and GTID+Binlog Position in MySQL), and notifies the client when the data has been replicated, as part of checkpoint data in the normal data stream. # PowerSync Service Source: https://docs.powersync.com/architecture/powersync-service Each PowerSync instance runs a copy of the PowerSync Service. The primary purpose of this service is to stream changes to clients. This service has the following components: ## Replication The service continuously replicates data from the source database, then: 1. Pre-processes the data according to the [sync rules](/usage/sync-rules) (both data queries and parameter queries), splitting data into [sync buckets](/usage/sync-rules/organize-data-into-buckets) and transforming the data if required. 2. Persists each operation into the relevant sync buckets, ready to be streamed to clients. The recent history of operations to each row is stored, not only the current version. This supports the "append-only" structure of sync buckets, which allows clients to efficiently stream changes while maintaining data integrity. Sync buckets can be compacted to avoid an ever-growing history. Replication is initially performed by taking a snapshot of all tables defined in the sync rules, then data is incrementally replicated using [logical replication](https://www.postgresql.org/docs/current/logical-replication.html). When sync rules are updated, this process restarts with a new snapshot. ## Authentication The service authenticates users using [JWTs](/installation/authentication-setup), before allowing access to data. ## Streaming Sync Once a user is authenticated: 1. The service calculates a list of buckets for the user to sync using [parameter queries](/usage/sync-rules/parameter-queries). 2. The service streams any operations added to those buckets since the last time the user connected. The service then continuously monitors for buckets that are added or removed, as well as for new operations within those buckets, and streams those changes. Only the internal (replicated) storage of the PowerSync Service is used — the source database is not queried directly during streaming. ## Source Code To access the source code for the PowerSync Service, refer to the [powersync-service](https://github.com/powersync-ja/powersync-service) repo on GitHub. ## See Also * [PowerSync Overview](/intro/powersync-overview) # Capacitor (alpha) Source: https://docs.powersync.com/client-sdk-references/capacitor Full SDK reference for using PowerSync in Capacitor clients This SDK is distributed via NPM Refer to `packages/capacitor` in the `powersync-js` repo on GitHub Full API reference for the SDK Gallery of example projects/demo apps built with Capacitor and PowerSync Changelog for the SDK This SDK is currently in an [**alpha** release](/resources/feature-status). The SDK is largely built on our stable [Web SDK](/client-sdk-references/javascript-web), so that functionality can be considered stable. However, the [Capacitor Community SQLite](https://github.com/capacitor-community/sqlite) integration for mobile platforms is in alpha for real-world testing and feedback. There are [known limitations](#limitations) currently. **Built on the Web SDK** The PowerSync Capacitor SDK is built on top of the [PowerSync Web SDK](/client-sdk-references/javascript-web). It shares the same API and usage patterns as the Web SDK. The main differences are: * Uses Capacitor-specific SQLite implementation (`@capacitor-community/sqlite`) for native Android and iOS platforms * Certain features are not supported on native Android and iOS platforms, see [limitations](#limitations) below for details All code examples from the Web SDK apply to Capacitor — use `@powersync/web` for imports instead of `@powersync/capacitor`. See the [JavaScript Web SDK reference](/client-sdk-references/javascript-web) for ORM support, SPA framework integration, and developer notes. ### SDK Features * **Real-time streaming of database changes**: Changes made by one user are instantly streamed to all other users with access to that data. This keeps clients automatically in sync without manual polling or refresh logic. * **Direct access to a local SQLite database**: Data is stored locally, so apps can read and write instantly without network calls. This enables offline support and faster user interactions. * **Asynchronous background execution**: The SDK performs database operations in the background to avoid blocking the application’s main thread. This means that apps stay responsive, even during heavy data activity. * **Query subscriptions for live updates**: The SDK supports query subscriptions that automatically push real-time updates to client applications as data changes, keeping your UI reactive and up to date. * **Automatic schema management**: PowerSync syncs schemaless data and applies a client-defined schema using SQLite views. This architecture means that PowerSync SDKs can handle schema changes gracefully without requiring explicit migrations on the client-side. ## Installation ## Install Package Add the [PowerSync Capacitor NPM package](https://www.npmjs.com/package/@powersync/capacitor) to your project: ```bash theme={null} npm install @powersync/capacitor ``` ```bash theme={null} yarn add @powersync/capacitor ``` ```bash theme={null} pnpm install @powersync/capacitor ``` This package uses [`@powersync/web`](https://www.npmjs.com/package/@powersync/web) as a peer dependency. For additional `@powersync/web` configuration and instructions see the [Web SDK README](/client-sdk-references/javascript-web). ## Install Peer Dependencies You must also install the following peer dependencies: ```bash theme={null} npm install @capacitor-community/sqlite @powersync/web @journeyapps/wa-sqlite ``` ```bash theme={null} yarn add @capacitor-community/sqlite @powersync/web @journeyapps/wa-sqlite ``` ```bash theme={null} pnpm install @capacitor-community/sqlite @powersync/web @journeyapps/wa-sqlite ``` See the [Capacitor Community SQLite repository](https://github.com/capacitor-community/sqlite) for additional instructions. ## Sync Capacitor Plugins After installing, sync your Capacitor project: ```bash theme={null} npx cap sync ``` ## Getting Started Before implementing the PowerSync SDK in your project, make sure you have completed these steps: * Signed up for a PowerSync Cloud account ([here](https://accounts.journeyapps.com/portal/powersync-signup?s=docs)) or [self-host PowerSync](/self-hosting/getting-started). * [Configured your backend database](/installation/database-setup) and connected it to your PowerSync instance. * [Installed](/client-sdk-references/capacitor#installation) the PowerSync Capacitor SDK. ### 1. Define the Schema The first step is defining the schema for the local SQLite database. This schema represents a "view" of the downloaded data. No migrations are required — the schema is applied directly when the local PowerSync database is constructed (as we'll show in the next step). **Generate schema automatically** In the [PowerSync Dashboard](https://dashboard.powersync.com/), select your project and instance and click the **Connect** button in the top bar to generate the client-side schema in your preferred language. The schema will be generated based off your Sync Rules. Similar functionality exists in the [CLI](/usage/tools/cli). **Note:** The generated schema will exclude an `id` column, as the client SDK automatically creates an `id` column of type `text`. Consequently, it is not necessary to specify an `id` column in your schema. For additional information on IDs, refer to [Client ID](/usage/sync-rules/client-id). The types available are `text`, `integer` and `real`. These should map directly to the values produced by the [Sync Rules](/usage/sync-rules). If a value doesn't match, it is cast automatically. For details on how Postgres types are mapped to the types below, see the section on [Types](/usage/sync-rules/types) in the *Sync Rules* documentation. **Example**: **Note on imports**: While you install `@powersync/capacitor`, the Capacitor SDK extends the Web SDK so you import general components from `@powersync/web` (installed as a peer dependency). See the [JavaScript Web SDK schema definition section](/client-sdk-references/javascript-web#1-define-the-schema) for more advanced examples. ```js theme={null} // AppSchema.ts import { column, Schema, Table } from '@powersync/web'; const lists = new Table({ created_at: column.text, name: column.text, owner_id: column.text }); 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, lists }); // For types export type Database = (typeof AppSchema)['types']; export type TodoRecord = Database['todos']; // OR: // export type Todo = RowType; export type ListRecord = Database['lists']; ``` **Note**: No need to declare a primary key `id` column, as PowerSync will automatically create this. ### 2. Instantiate the PowerSync Database Next, you need to instantiate the PowerSync database — this is the core managed database. Its primary functions are to record all changes in the local database, whether online or offline. In addition, it automatically uploads changes to your app backend when connected. **Example**: The Capacitor PowerSyncDatabase automatically detects the platform and uses the appropriate database drivers: * **Android and iOS**: Uses [Capacitor Community SQLite](https://github.com/capacitor-community/sqlite) for native database access * **Web**: Falls back to the PowerSync Web SDK ```js theme={null} 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' } }); ``` When using custom database factories, be sure to specify the `CapacitorSQLiteOpenFactory` for Capacitor platforms: ```js theme={null} import { PowerSyncDatabase } from '@powersync/capacitor'; import { WASQLiteOpenFactory, CapacitorSQLiteOpenFactory } from '@powersync/capacitor'; import { Schema } from '@powersync/web'; const db = new PowerSyncDatabase({ schema: AppSchema, database: isWeb ? new WASQLiteOpenFactory({ dbFilename: "mydb.sqlite" }) : new CapacitorSQLiteOpenFactory({ dbFilename: "mydb.sqlite" }) }); ``` Once you've instantiated your PowerSync database, you will need to call the [connect()](https://powersync-ja.github.io/powersync-js/web-sdk/classes/AbstractPowerSyncDatabase#connect) method to activate it. ```js theme={null} export const setupPowerSync = async () => { // Uses the backend connector that will be created in the next section const connector = new Connector(); db.connect(connector); }; ``` ### 3. Integrate with your Backend The PowerSync backend connector provides the connection between your application backend and the PowerSync client-side managed SQLite database. It is used to: 1. Retrieve an auth token to connect to the PowerSync instance. 2. Apply local changes on your backend application server (and from there, to your backend database) Accordingly, the connector must implement two methods: 1. [PowerSyncBackendConnector.fetchCredentials](https://github.com/powersync-ja/powersync-js/blob/ed5bb49b5a1dc579050304fab847feb8d09b45c7/packages/common/src/client/connection/PowerSyncBackendConnector.ts#L16) - This is called every couple of minutes and is used to obtain credentials for your app backend API. -> See [Authentication Setup](/installation/authentication-setup) for instructions on how the credentials should be generated. 2. [PowerSyncBackendConnector.uploadData](https://github.com/powersync-ja/powersync-js/blob/ed5bb49b5a1dc579050304fab847feb8d09b45c7/packages/common/src/client/connection/PowerSyncBackendConnector.ts#L24) - Use this to upload client-side changes to your app backend. -> See [Writing Client Changes](/installation/app-backend-setup/writing-client-changes) for considerations on the app backend implementation. **Example**: See the [JavaScript Web SDK backend integration section](/client-sdk-references/javascript-web#3-integrate-with-your-backend) for connector examples with Supabase and Firebase authentication, and handling `uploadData` with batch operations. ```js theme={null} import { UpdateType } from '@powersync/web'; export class Connector { async fetchCredentials() { // Implement fetchCredentials to obtain a JWT from your authentication service. // See https://docs.powersync.com/installation/authentication-setup // If you're using Supabase or Firebase, you can re-use the JWT from those clients, see // - https://docs.powersync.com/installation/authentication-setup/supabase-auth // - https://docs.powersync.com/installation/authentication-setup/firebase-auth return { endpoint: '[Your PowerSync instance URL or self-hosted endpoint]', // Use a development token (see Authentication Setup https://docs.powersync.com/installation/authentication-setup/development-tokens) to get up and running quickly token: 'An authentication token' }; } async uploadData(database) { // Implement uploadData to send local changes to your backend service. // You can omit this method if you only want to sync data from the database to the client // See example implementation here: https://docs.powersync.com/client-sdk-references/javascript-web#3-integrate-with-your-backend } } ``` ## Using PowerSync: CRUD functions Once the PowerSync instance is configured you can start using the SQLite DB functions. **All CRUD examples from the JavaScript Web SDK apply**: The Capacitor SDK uses the same API as the Web SDK. See the [JavaScript Web SDK CRUD functions section](/client-sdk-references/javascript-web#using-powersync-crud-functions) for examples of `get`, `getAll`, `watch`, `execute`, `writeTransaction`, incremental watch updates, and differential results. The most commonly used CRUD functions to interact with your SQLite data are: * [PowerSyncDatabase.get](/client-sdk-references/javascript-web#fetching-a-single-item) - get (SELECT) a single row from a table. * [PowerSyncDatabase.getAll](/client-sdk-references/javascript-web#querying-items-powersync.getall) - get (SELECT) a set of rows from a table. * [PowerSyncDatabase.watch](/client-sdk-references/javascript-web#watching-queries-powersync.watch) - execute a read query every time source tables are modified. * [PowerSyncDatabase.execute](/client-sdk-references/javascript-web#mutations-powersync.execute) - execute a write (INSERT/UPDATE/DELETE) query. ### Fetching a Single Item The [get](https://powersync-ja.github.io/powersync-js/web-sdk/classes/PowerSyncDatabase#get) method executes a read-only (SELECT) query and returns a single result. It throws an exception if no result is found. Use [getOptional](https://powersync-ja.github.io/powersync-js/web-sdk/classes/PowerSyncDatabase#getoptional) to return a single optional result (returns `null` if no result is found). ```js theme={null} // Find a list item by ID export const findList = async (id) => { const result = await db.get('SELECT * FROM lists WHERE id = ?', [id]); return result; } ``` ### Querying Items (PowerSync.getAll) The [getAll](https://powersync-ja.github.io/powersync-js/web-sdk/classes/PowerSyncDatabase#getall) method returns a set of rows from a table. ```js theme={null} // Get all list IDs export const getLists = async () => { const results = await db.getAll('SELECT * FROM lists'); return results; } ``` ### Watching Queries (PowerSync.watch) The [watch](https://powersync-ja.github.io/powersync-js/web-sdk/classes/PowerSyncDatabase#watch) method executes a read query whenever a change to a dependent table is made. ```javascript theme={null} async function* pendingLists(): AsyncIterable { for await (const result of db.watch( `SELECT * FROM lists WHERE state = ?`, ['pending'] )) { yield result.rows?._array ?? []; } } ``` ```javascript theme={null} const pendingLists = (onResult: (lists: any[]) => void): void => { db.watch( 'SELECT * FROM lists WHERE state = ?', ['pending'], { onResult: (result: any) => { onResult(result.rows?._array ?? []); } } ); } ``` For advanced watch query features like incremental updates and differential results, see [Live Queries / Watch Queries](/usage/use-case-examples/watch-queries). ### Mutations (PowerSync.execute, PowerSync.writeTransaction) The [execute](https://powersync-ja.github.io/powersync-js/web-sdk/classes/PowerSyncDatabase#execute) method can be used for executing single SQLite write statements. ```js theme={null} // Delete a list item by ID export const deleteList = async (id) => { const result = await db.execute('DELETE FROM lists WHERE id = ?', [id]); return TodoList.fromRow(results); } // OR: using a transaction const deleteList = async (id) => { await db.writeTransaction(async (tx) => { // Delete associated todos await tx.execute(`DELETE FROM ${TODOS_TABLE} WHERE list_id = ?`, [id]); // Delete list record await tx.execute(`DELETE FROM ${LISTS_TABLE} WHERE id = ?`, [id]); }); }; ``` ## Configure Logging ```js theme={null} import { createBaseLogger, LogLevel } from '@powersync/web'; const logger = createBaseLogger(); // Configure the logger to use the default console output logger.useDefaults(); // Set the minimum log level to DEBUG to see all log messages // Available levels: DEBUG, INFO, WARN, ERROR, TRACE, OFF logger.setLevel(LogLevel.DEBUG); ``` Enable verbose output in the developer tools for detailed logs. ## Limitations * Encryption for native mobile platforms is not yet supported. * Multiple tab support is not available for native Android and iOS targets. * `PowerSyncDatabase.executeRaw` does not support results where multiple columns would have the same name in SQLite * `PowerSyncDatabase.execute` has limited support on Android. The SQLCipher Android driver exposes queries and executions as separate APIs, so there is no single method that handles both. While PowerSyncDatabase.execute accepts both, on Android we treat a statement as a query only when the SQL starts with select (case-insensitive). ## Additional Resources See the [JavaScript Web SDK reference](/client-sdk-references/javascript-web) for: * [ORM Support](/client-sdk-references/javascript-web/javascript-orm/overview) * [SPA Framework Integration](/client-sdk-references/javascript-web/javascript-spa-frameworks) * [Usage Examples](/client-sdk-references/javascript-web/usage-examples) * [Developer Notes](/client-sdk-references/javascript-web#developer-notes) ## Troubleshooting See [Troubleshooting](/resources/troubleshooting) for pointers to debug common issues. ## Supported Platforms See [Supported Platforms -> Capacitor SDK](/resources/supported-platforms#capacitor-sdk). # JavaScript ORM Support Source: https://docs.powersync.com/client-sdk-references/capacitor/javascript-orm-support # .NET (alpha) Source: https://docs.powersync.com/client-sdk-references/dotnet SDK reference for using PowerSync in .NET clients. This SDK is distributed via NuGet Refer to the `powersync-dotnet` repo on GitHub A full API Reference for this SDK is not yet available. This is planned for a future release. Gallery of example projects/demo apps built with .NET PowerSync Changelog for the SDK This SDK is currently in an [**alpha** release](/resources/feature-status). It is not suitable for production use as breaking changes may still occur. ## Supported Frameworks and Targets The PowerSync .NET SDK supports: * **.NET Versions**: 6, 8, and 9 * **.NET Framework**: Version 4.8 (requires additional configuration) * **MAUI**: Cross-platform support for Android, iOS, and Windows * **WPF**: Windows desktop applications **Current Limitations**: * Blazor (web) platforms are not yet supported. For more details, please refer to the package [README](https://github.com/powersync-ja/powersync-dotnet/tree/main?tab=readme-ov-file). ## SDK Features * Provides real-time streaming of database changes. * Offers direct access to the SQLite database, enabling the use of SQL on both client and server sides. * Provides watched queries that allow listening for live updates to data. * Eliminates the need for client-side database migrations as these are managed automatically. ## Quickstart For desktop/server/binary use-cases and WPF, add the [`PowerSync.Common`](https://www.nuget.org/packages/PowerSync.Common/) NuGet package to your project: ```bash theme={null} dotnet add package PowerSync.Common --prerelease ``` For MAUI apps, add both [`PowerSync.Common`](https://www.nuget.org/packages/PowerSync.Common/) and [`PowerSync.Maui`](https://www.nuget.org/packages/PowerSync.Maui/) NuGet packages to your project: ```bash theme={null} dotnet add package PowerSync.Common --prerelease dotnet add package PowerSync.Maui --prerelease ``` Add `--prerelease` while this package is in alpha. Next, make sure that you have: * Signed up for a PowerSync Cloud account ([here](https://accounts.journeyapps.com/portal/powersync-signup?s=docs)) or [self-host PowerSync](/self-hosting/getting-started). * [Configured your backend database](/installation/database-setup) and connected it to your PowerSync instance. ### 1. Define the schema The first step is defining the schema for the local SQLite database. This schema represents a "view" of the downloaded data. No migrations are required — the schema is applied directly when the local PowerSync database is constructed (as we'll show in the next step). **Generate schema automatically** In the [PowerSync Dashboard](https://dashboard.powersync.com/), select your project and instance and click the **Connect** button in the top bar to generate the client-side schema in your preferred language. The schema will be generated based off your Sync Rules. Similar functionality exists in the [CLI](/usage/tools/cli). **Note:** The generated schema will exclude an `id` column, as the client SDK automatically creates an `id` column of type `text`. Consequently, it is not necessary to specify an `id` column in your schema. For additional information on IDs, refer to [Client ID](/usage/sync-rules/client-id). You can use [this example](https://github.com/powersync-ja/powersync-dotnet/blob/main/demos/CommandLine/AppSchema.cs) as a reference when defining your schema. ### 2. Instantiate the PowerSync Database Next, you need to instantiate the PowerSync database — this is the core managed database. Its primary functions are to record all changes in the local database, whether online or offline. In addition, it automatically uploads changes to your app backend when connected. **Example**: The initialization syntax differs slightly between the Common and MAUI SDKs: ```cs theme={null} 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(); } } ``` ```cs theme={null} 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(); } } ``` ### 3. Integrate with your Backend The PowerSync backend connector provides the connection between your application backend and the PowerSync client-side managed SQLite database. It is used to: 1. Retrieve an auth token to connect to the PowerSync instance. 2. Apply local changes on your backend application server (and from there, to your backend database) Accordingly, the connector must implement two methods: 1. [PowerSyncBackendConnector.FetchCredentials](https://github.com/powersync-ja/powersync-dotnet/blob/main/demos/CommandLine/NodeConnector.cs#L50) - This is called every couple of minutes and is used to obtain credentials for your app backend API. -> See [Authentication Setup](/installation/authentication-setup) for instructions on how the credentials should be generated. 2. [PowerSyncBackendConnector.UploadData](https://github.com/powersync-ja/powersync-dotnet/blob/main/demos/CommandLine/NodeConnector.cs#L72) - Use this to upload client-side changes to your app backend. -> See [Writing Client Changes](/installation/app-backend-setup/writing-client-changes) for considerations on the app backend implementation. **Example**: ```cs theme={null} 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 { private readonly HttpClient _httpClient; // User credentials for the current session public string UserId { get; private set; } // Service endpoints private readonly string _backendUrl; private readonly string _powerSyncUrl; private string? _clientId; public MyConnector() { _httpClient = new HttpClient(); // In a real app, this would come from your authentication system UserId = "user-123"; // Configure your service endpoints _backendUrl = "https://your-backend-api.example.com"; _powerSyncUrl = "https://your-powersync-instance.powersync.journeyapps.com"; } public async Task FetchCredentials() { try { // Obtain a JWT from your authentication service. // See https://docs.powersync.com/installation/authentication-setup // If you're using Supabase or Firebase, you can re-use the JWT from those clients, see // - https://docs.powersync.com/installation/authentication-setup/supabase-auth // - https://docs.powersync.com/installation/authentication-setup/firebase-auth var authToken = "your-auth-token"; // Use a development token (see Authentication Setup https://docs.powersync.com/installation/authentication-setup/development-tokens) to get up and running quickly // Return credentials with PowerSync endpoint and JWT token return new PowerSyncCredentials(_powerSyncUrl, authToken); } catch (Exception ex) { Console.WriteLine($"Error fetching credentials: {ex.Message}"); throw; } } public async Task UploadData(IPowerSyncDatabase database) { // Get the next transaction to upload CrudTransaction? transaction; try { transaction = await database.GetNextCrudTransaction(); } catch (Exception ex) { Console.WriteLine($"UploadData Error: {ex.Message}"); return; } // If there's no transaction, there's nothing to upload if (transaction == null) { return; } // Get client ID if not already retrieved _clientId ??= await database.GetClientId(); try { // Convert PowerSync operations to your backend format var batch = new List(); foreach (var operation in transaction.Crud) { batch.Add(new { op = operation.Op.ToString(), // INSERT, UPDATE, DELETE table = operation.Table, id = operation.Id, data = operation.OpData }); } // Send the operations to your backend var payload = JsonSerializer.Serialize(new { batch }); var content = new StringContent(payload, Encoding.UTF8, "application/json"); HttpResponseMessage response = await _httpClient.PostAsync($"{_backendUrl}/api/data", content); response.EnsureSuccessStatusCode(); // Mark the transaction as completed await transaction.Complete(); } catch (Exception ex) { Console.WriteLine($"UploadData Error: {ex.Message}"); throw; } } } ``` With your database instantiated and your connector ready, call `connect` to start the synchronization process: ```cs theme={null} await db.Connect(new MyConnector()); await db.WaitForFirstSync(); // Optional, to wait for a complete snapshot of data to be available ``` ## Usage After connecting the client database, it is ready to be used. You can run queries and make updates as follows: ```cs theme={null} // Use db.Get() to fetch a single row: Console.WriteLine(await db.Get("SELECT powersync_rs_version();")); // Or db.GetAll() to fetch all: // Where List result is defined: // record ListResult(string id, string name, string owner_id, string created_at); Console.WriteLine(await db.GetAll("SELECT * FROM lists;")); // Use db.Watch() to watch queries for changes (await is used to wait for initialization): // And db.Execute for inserts, updates and deletes: await db.Execute( "insert into lists (id, name, owner_id, created_at) values (uuid(), 'New User', ?, datetime())", [connector.UserId] ); ``` ## Configure Logging Enable logging to help you debug your app. By default, the SDK uses a no-op logger that doesn't output any logs. To enable logging, you can configure a custom logger using .NET's `ILogger` interface: ```cs theme={null} using Microsoft.Extensions.Logging; using PowerSync.Common.Client; // Create a logger factory ILoggerFactory loggerFactory = LoggerFactory.Create(builder => { builder.AddConsole(); // Enable console logging builder.SetMinimumLevel(LogLevel.Information); // Set minimum log level }); var logger = loggerFactory.CreateLogger("PowerSyncLogger"); var db = new PowerSyncDatabase(new PowerSyncDatabaseOptions { Database = new SQLOpenOptions { DbFilename = "powersync.db" }, Schema = AppSchema.PowerSyncSchema, Logger = logger }); ``` ## Supported Platforms See [Supported Platforms -> .NET SDK](/resources/supported-platforms#net-sdk). # Dart/Flutter Source: https://docs.powersync.com/client-sdk-references/flutter Full SDK reference for using PowerSync in Dart/Flutter clients The SDK is distributed via pub.dev Refer to the `powersync.dart` repo on GitHub Full API reference for the SDK Gallery of example projects/demo apps built with Flutter and PowerSync Changelog for the SDK ### Quickstart Get started quickly by using the self-hosted **Flutter** + **Supabase** template 📂 GitHub Repo [https://github.com/powersync-community/flutter-powersync-supabase](https://github.com/powersync-community/flutter-powersync-supabase) ### SDK Features * **Real-time streaming of database changes**: Changes made by one user are instantly streamed to all other users with access to that data. This keeps clients automatically in sync without manual polling or refresh logic. * **Direct access to a local SQLite database**: Data is stored locally, so apps can read and write instantly without network calls. This enables offline support and faster user interactions. * **Asynchronous background execution**: The SDK performs database operations in the background to avoid blocking the application’s main thread. This means that apps stay responsive, even during heavy data activity. * **Query subscriptions for live updates**: The SDK supports query subscriptions that automatically push real-time updates to client applications as data changes, keeping your UI reactive and up to date. * **Automatic schema management**: PowerSync syncs schemaless data and applies a client-defined schema using SQLite views. This architecture means that PowerSync SDKs can handle schema changes gracefully without requiring explicit migrations on the client-side. Web support is currently in a beta release. Refer to [Flutter Web Support](/client-sdk-references/flutter/flutter-web-support) for more details. ## Installation Add the [PowerSync pub.dev package](https://pub.dev/packages/powersync) to your project: ```bash theme={null} flutter pub add powersync ``` ## Getting Started Before implementing the PowerSync SDK in your project, make sure you have completed these steps: * Signed up for a PowerSync Cloud account ([here](https://accounts.journeyapps.com/portal/powersync-signup?s=docs)) or [self-host PowerSync](/self-hosting/getting-started). * [Configured your backend database](/installation/database-setup) and connected it to your PowerSync instance. * [Installed](/client-sdk-references/flutter#installation) the PowerSync Dart/Flutter SDK. For this reference document, we assume that you have created a Flutter project and have the following directory structure: ```plaintext theme={null} lib/ ├── models/ ├── schema.dart └── todolist.dart ├── powersync/ ├── my_backend_connector.dart └── powersync.dart ├── widgets/ ├── lists_widget.dart ├── todos_widget.dart ├── main.dart ``` ### 1. Define the Schema The first step is defining the schema for the local SQLite database. This will be provided as a `schema` parameter to the [PowerSyncDatabase](https://pub.dev/documentation/powersync/latest/powersync/PowerSyncDatabase/PowerSyncDatabase.html) constructor. This schema represents a "view" of the downloaded data. No migrations are required — the schema is applied directly when the PowerSync database is constructed. **Generate schema automatically** In the [PowerSync Dashboard](https://dashboard.powersync.com/), select your project and instance and click the **Connect** button in the top bar to generate the client-side schema in your preferred language. The schema will be generated based off your Sync Rules. Similar functionality exists in the [CLI](/usage/tools/cli). **Note:** The generated schema will exclude an `id` column, as the client SDK automatically creates an `id` column of type `text`. Consequently, it is not necessary to specify an `id` column in your schema. For additional information on IDs, refer to [Client ID](/usage/sync-rules/client-id). The types available are `text`, `integer` and `real`. These should map directly to the values produced by the [Sync Rules](/usage/sync-rules). If a value doesn't match, it is cast automatically. For details on how Postgres types are mapped to the types below, see the section on [Types](/usage/sync-rules/types) in the *Sync Rules* documentation. **Example**: ```dart lib/models/schema.dart theme={null} 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 to allow efficient lookup within a list Index('list', [IndexedColumn('list_id')]) ]), Table('lists', [ Column.text('created_at'), Column.text('name'), Column.text('owner_id') ]) ])); ``` **Note**: No need to declare a primary key `id` column, as PowerSync will automatically create this. ### 2. Instantiate the PowerSync Database Next, you need to instantiate the PowerSync database — this is the core managed client-side database. Its primary functions are to record all changes in the local database, whether online or offline. In addition, it automatically uploads changes to your app backend when connected. To instantiate `PowerSyncDatabase`, inject the Schema you defined in the previous step and a file path — it's important to only instantiate one instance of `PowerSyncDatabase` per file. **Example**: ```dart lib/powersync/powersync.dart theme={null} import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:powersync/powersync.dart'; import '../main.dart'; import '../models/schema.dart'; openDatabase() async { final dir = await getApplicationSupportDirectory(); final path = join(dir.path, 'powersync-dart.db'); // Set up the database // Inject the Schema you defined in the previous step and a file path db = PowerSyncDatabase(schema: schema, path: path); await db.initialize(); } ``` Once you've instantiated your PowerSync database, you will need to call the [connect()](https://pub.dev/documentation/powersync/latest/powersync/PowerSyncDatabase/connect.html) method to activate it. This method requires the backend connector that will be created in the next step. ```dart lib/main.dart {35} theme={null} import 'package:flutter/material.dart'; import 'package:powersync/powersync.dart'; import 'powersync/powersync.dart'; late PowerSyncDatabase db; Future main() async { WidgetsFlutterBinding.ensureInitialized(); await openDatabase(); runApp(const DemoApp()); } class DemoApp extends StatefulWidget { const DemoApp({super.key}); @override State createState() => _DemoAppState(); } class _DemoAppState extends State { @override Widget build(BuildContext context) { return MaterialApp( title: 'Demo', home: // TODO: Implement your own UI here. // You could listen for authentication state changes to connect or disconnect from PowerSync StreamBuilder( stream: // TODO: some stream, builder: (ctx, snapshot) {, // TODO: implement your own condition here if ( ... ) { // Uses the backend connector that will be created in the next step db.connect(connector: MyBackendConnector()); // TODO: implement your own UI here } }, ) ); } } ``` ### 3. Integrate with your Backend The PowerSync backend connector provides the connection between your application backend and the PowerSync client-side managed SQLite database. It is used to: 1. [Retrieve an auth token](/installation/authentication-setup) to connect to the PowerSync instance. 2. [Apply local changes](/installation/app-backend-setup/writing-client-changes) on your backend application server (and from there, to your backend database) Accordingly, the connector must implement two methods: 1. [PowerSyncBackendConnector.fetchCredentials](https://pub.dev/documentation/powersync/latest/powersync/PowerSyncBackendConnector/fetchCredentials.html) - This is called every couple of minutes and is used to obtain credentials for your app backend API. -> See [Authentication Setup](/installation/authentication-setup) for instructions on how the credentials should be generated. 2. [PowerSyncBackendConnector.uploadData](https://pub.dev/documentation/powersync/latest/powersync/PowerSyncBackendConnector/uploadData.html) - Use this to upload client-side changes to your app backend. -> See [Writing Client Changes](/installation/app-backend-setup/writing-client-changes) for considerations on the app backend implementation. **Example**: ```dart lib/powersync/my_backend_connector.dart theme={null} import 'package:powersync/powersync.dart'; class MyBackendConnector extends PowerSyncBackendConnector { PowerSyncDatabase db; MyBackendConnector(this.db); @override Future fetchCredentials() async { // Implement fetchCredentials to obtain a JWT from your authentication service // If you're using Supabase or Firebase, you can re-use the JWT from those clients, see // - https://docs.powersync.com/installation/authentication-setup/supabase-auth // - https://docs.powersync.com/installation/authentication-setup/firebase-auth // See example implementation here: https://pub.dev/documentation/powersync/latest/powersync/DevConnector/fetchCredentials.html return PowerSyncCredentials( endpoint: 'https://xxxxxx.powersync.journeyapps.com', // Use a development token (see Authentication Setup https://docs.powersync.com/installation/authentication-setup/development-tokens) to get up and running quickly token: 'An authentication token' ); } // Implement uploadData to send local changes to your backend service // You can omit this method if you only want to sync data from the server to the client // See example implementation here: https://docs.powersync.com/client-sdk-references/flutter#3-integrate-with-your-backend @override Future uploadData(PowerSyncDatabase database) async { // This function is called whenever there is data to upload, whether the // device is online or offline. // If this call throws an error, it is retried periodically. final transaction = await database.getNextCrudTransaction(); if (transaction == null) { return; } // The data that needs to be changed in the remote db for (var op in transaction.crud) { switch (op.op) { case UpdateType.put: // TODO: Instruct your backend API to CREATE a record case UpdateType.patch: // TODO: Instruct your backend API to PATCH a record case UpdateType.delete: //TODO: Instruct your backend API to DELETE a record } } // Completes the transaction and moves onto the next one await transaction.complete(); } } ``` ## Using PowerSync: CRUD functions Once the PowerSync instance is configured you can start using the SQLite DB functions. The most commonly used CRUD functions to interact with your SQLite data are: * [PowerSyncDatabase.get](/client-sdk-references/flutter#fetching-a-single-item) - get (SELECT) a single row from a table. * [PowerSyncDatabase.getAll](/client-sdk-references/flutter#querying-items-powersync.getall) - get (SELECT) a set of rows from a table. * [PowerSyncDatabase.watch](/client-sdk-references/flutter#watching-queries-powersync.watch) - execute a read query every time source tables are modified. * [PowerSyncDatabase.execute](/client-sdk-references/flutter#mutations-powersync.execute) - execute a write (INSERT/UPDATE/DELETE) query. For the following examples, we will define a `TodoList` model class that represents a List of todos. ```dart lib/models/todolist.dart theme={null} /// This is a simple model class representing a TodoList class TodoList { final int id; final String name; final DateTime createdAt; final DateTime updatedAt; TodoList({ required this.id, required this.name, required this.createdAt, required this.updatedAt, }); factory TodoList.fromRow(Map row) { return TodoList( id: row['id'], name: row['name'], createdAt: DateTime.parse(row['created_at']), updatedAt: DateTime.parse(row['updated_at']), ); } } ``` ### Fetching a Single Item The [get](https://pub.dev/documentation/powersync/latest/sqlite_async/SqliteQueries/get.html) method executes a read-only (SELECT) query and returns a single result. It throws an exception if no result is found. Use [getOptional](https://pub.dev/documentation/powersync/latest/sqlite_async/SqliteQueries/getOptional.html) to return a single optional result (returns `null` if no result is found). The following is an example of selecting a list item by ID: ```dart lib/widgets/lists_widget.dart theme={null} import '../main.dart'; import '../models/todolist.dart'; Future find(id) async { final result = await db.get('SELECT * FROM lists WHERE id = ?', [id]); return TodoList.fromRow(result); } ``` ### Querying Items (PowerSync.getAll) The [getAll](https://pub.dev/documentation/powersync/latest/sqlite_async/SqliteQueries/getAll.html) method returns a set of rows from a table. ```dart lib/widgets/lists_widget.dart theme={null} import 'package:powersync/sqlite3.dart'; import '../main.dart'; Future> getLists() async { ResultSet results = await db.getAll('SELECT id FROM lists WHERE id IS NOT NULL'); List ids = results.map((row) => row['id'] as String).toList(); return ids; } ``` ### Watching Queries (PowerSync.watch) The [watch](https://pub.dev/documentation/powersync/latest/sqlite_async/SqliteQueries/watch.html) method executes a read query whenever a change to a dependent table is made. ```dart theme={null} StreamBuilder( stream: db.watch('SELECT * FROM lists WHERE state = ?', ['pending']), builder: (context, snapshot) { if (snapshot.hasData) { // TODO: implement your own UI here based on the result set return ...; } else { return const Center(child: CircularProgressIndicator()); } }, ) ``` ### Mutations (PowerSync.execute) The [execute](https://pub.dev/documentation/powersync/latest/powersync/PowerSyncDatabase/execute.html) method can be used for executing single SQLite write statements. ```dart lib/widgets/todos_widget.dart {12-15} theme={null} import 'package:flutter/material.dart'; import '../main.dart'; // Example Todos widget class TodosWidget extends StatelessWidget { const TodosWidget({super.key}); @override Widget build(BuildContext context) { return FloatingActionButton( onPressed: () async { await db.execute( 'INSERT INTO lists(id, created_at, name, owner_id) VALUES(uuid(), datetime(), ?, ?)', ['name', '123'], ); }, tooltip: '+', child: const Icon(Icons.add), ); } } ``` ## Configure Logging Since version 1.1.2 of the SDK, logging is enabled by default and outputs logs from PowerSync to the console in debug mode. ## Additional Usage Examples See [Usage Examples](/client-sdk-references/flutter/usage-examples) for further examples of the SDK. ## ORM Support See [Flutter ORM Support](/client-sdk-references/flutter/flutter-orm-support) for details. ## Troubleshooting See [Troubleshooting](/resources/troubleshooting) for pointers to debug common issues. ## Supported Platforms See [Supported Platforms -> Dart SDK](/resources/supported-platforms#dart-sdk). # API Reference Source: https://docs.powersync.com/client-sdk-references/flutter/api-reference # Encryption Source: https://docs.powersync.com/client-sdk-references/flutter/encryption # Flutter ORM Support Source: https://docs.powersync.com/client-sdk-references/flutter/flutter-orm-support An introduction to using ORMs with PowerSync is available on our blog [here](https://www.powersync.com/blog/using-orms-with-powersync). ORM support is available via the following package (currently in a beta release): This package enables using the [Drift](https://pub.dev/packages/drift) persistence library (ORM) with the PowerSync Dart/Flutter SDK. The Drift integration gives Flutter developers the flexibility to write queries in either Dart or SQL. Importantly, it supports propagating change notifications from the PowerSync side to Drift, which is necessary for streaming queries. The use of this package is recommended for Flutter developers who already know Drift, or specifically want the benefits of an ORM for their PowerSync projects. ### Example implementation An example project which showcases setting up and using Drift with PowerSync is available here: ### Support for Other Flutter ORMs Other ORMs for Flutter, like [Floor](https://pinchbv.github.io/floor/), are not currently supported. It is technically possible to open a separate connection to the same database file using Floor but there are two big caveats to that: **Write locks** Every write transaction (or write statement) will lock the database for other writes for the duration of the transaction. While transactions are typically short, if multiple happen to run at the same time they may fail with a SQLITE\_BUSY or similar error. **External modifications** Often, ORMs only detect notifications made using the same library. In order to support streaming queries, PowerSync requires the ORM to allow external modifications to trigger the same change notifications, meaning streaming queries are unlikely to work out-of-the-box. # Flutter Web Support (Beta) Source: https://docs.powersync.com/client-sdk-references/flutter/flutter-web-support Web support for Flutter in version `^1.9.0` is currently in a **beta** release. It is functionally ready for production use, provided that you've tested your use cases. Please see the [Limitations](#limitations) detailed below. ## Demo app The easiest way to test Flutter Web support is to run the [Supabase Todo-List](https://github.com/powersync-ja/powersync.dart/tree/main/demos/supabase-todolist) demo app: 1. Clone the [powersync.dart](https://github.com/powersync-ja/powersync.dart/tree/main) repo. 1. **Note**: If you are an existing user updating to the latest code after a git pull, run `melos exec 'flutter pub upgrade'` in the repo's root and make sure it succeeds. 2. Run `melos prepare` in the repo's root 3. cd into the `demos/supabase-todolist` folder 4. If you haven’t yet: `cp lib/app_config_template.dart lib/app_config.dart` (optionally update this config with your own Supabase and PowerSync project details). 5. Run `flutter run -d chrome` ## Installing PowerSync in your own project Install the [latest version](https://pub.dev/packages/powersync/versions) of the package, for example: ```bash theme={null} flutter pub add powersync:'^1.9.0' ``` ### Additional config #### Assets Web support requires `sqlite3.wasm` and worker (`powersync_db.worker.js` and `powersync_sync.worker.js`) assets to be served from the web application. They can be downloaded to the web directory by running the following command in your application's root folder. ```bash theme={null} dart run powersync:setup_web ``` The same code is used for initializing native and web `PowerSyncDatabase` clients. #### OPFS for improved performance This SDK supports different storage modes of the SQLite database with varying levels of performance and compatibility: * **IndexedDB**: Highly compatible with different browsers, but performance is slow. * **OPFS** (Origin-Private File System): Significantly faster but requires additional configuration. OPFS is the preferred mode when it is available. Otherwise database storage falls back to IndexedDB. Enabling OPFS requires adding two headers to the HTTP server response when a client requests the Flutter web application: * `Cross-Origin-Opener-Policy`: Needs to be set to `same-origin`. * `Cross-Origin-Embedder-Policy`: Needs to be set to `require-corp`. When running the app locally, you can use the following command to include the required headers: ```bash theme={null} flutter run -d chrome --web-header "Cross-Origin-Opener-Policy=same-origin" --web-header "Cross-Origin-Embedder-Policy=require-corp" ``` When serving a Flutter Web app in production, the [Flutter docs](https://docs.flutter.dev/deployment/web#building-the-app-for-release) recommend building the web app with `flutter build web`, then serving the content with an HTTP server. The server should be configured to use the above headers. **Further reading**: [Drift](https://drift.simonbinder.eu/) uses the same packages as our [`sqlite_async`](https://github.com/powersync-ja/sqlite_async.dart) package under the hood, and has excellent documentation for how the web filesystem is selected. See [here](https://drift.simonbinder.eu/platforms/web/) for web compatibility notes and [here](https://drift.simonbinder.eu/platforms/web/#additional-headers) for additional notes on the required web headers. ## Limitations The API for Web is essentially the same as for native platforms, however, some features within `PowerSyncDatabase` clients are not available. ### Imports Flutter Web does not support importing directly from `sqlite3.dart` as it uses `dart:ffi`. Change imports from: ```dart theme={null} import 'package/powersync/sqlite3.dart` ``` to: ```dart theme={null} import 'package/powersync/sqlite3_common.dart' ``` in code which needs to run on the Web platform. Isolated native-specific code can still import from `sqlite3.dart`. ### Database connections Web database connections do not support concurrency. A single database connection is used. `readLock` and `writeLock` contexts do not implement checks for preventing writable queries in read connections and vice-versa. Direct access to the synchronous `CommonDatabase` (`sqlite.Database` equivalent for web) connection is not available. `computeWithDatabase` is not available on web. # State Management Source: https://docs.powersync.com/client-sdk-references/flutter/state-management Guidance on using PowerSync with popular Flutter state management libraries. Our [demo apps](/resources/demo-apps-example-projects) for Flutter are intentionally kept simple to put a focus on demonstrating PowerSync APIs. Instead of using heavy state management solutions, they use simple global fields to make the PowerSync database accessible to widgets. When adopting PowerSync, you might be interested in using a more sophisticated approach for state management. This section explains how PowerSync's Dart/Flutter SDK integrates with popular packages for state management. Adopting PowerSync can simplify the architecture of your app by using a local SQLite database as the single source of truth for all data. For a general discussion on how PowerSync fits into modern app architecture on Flutter, also see [this blogpost](https://dinkomarinac.dev/building-local-first-flutter-apps-with-riverpod-drift-and-powersync). PowerSync exposes database queries with the standard `Future` and `Stream` classes from `dart:async`. Given how widely used these are in the Dart ecosystem, PowerSync works well with all popular approaches for state management, such as: 1. Providers with `package:provider`: Create your database as a `Provider` and expose watched queries to child widgets with `StreamProvider`! The provider for databases should `close()` the database in `dispose`. 2. Providers with `package:riverpod`: We mention relevant snippets [below](#riverpod). 3. Dependency injection with `package:get_it`: PowerSync databases can be registered with `registerSingletonAsync`. Again, make sure to `close()` the database in the `dispose` callback. 4. The BLoC pattern with the `bloc` package: You can easily listen to watched queries in Cubits (although, if you find your Blocs and Cubits becoming trivial wrappers around database streams, consider just `watch()`ing database queries in widgets directly. That doesn't make your app [less testable](/client-sdk-references/flutter/unit-testing)!). To simplify state management, avoid the use of hydrated blocs and cubits for state that depends on database queries. With PowerSync, regular data is already available locally and doesn't need a second local cache. ## Riverpod We have a [complete example](https://github.com/powersync-ja/powersync.dart/tree/main/demos/supabase-todolist-drift) on using PowerSync with modern Flutter libraries like Riverpod, Drift and `auto_route`. A good way to open PowerSync databases with Riverpod is to use an async provider. You can also manage your `connect` and `disconnect` calls there, for instance by listening to the authentication state: ```dart theme={null} @Riverpod(keepAlive: true) Future powerSyncInstance(Ref ref) async { final db = PowerSyncDatabase( schema: schema, path: await _getDatabasePath(), logger: attachedLogger, ); await db.initialize(); // TODO: Listen for auth changes and connect() the database here. ref.listen(yourAuthProvider, (prev, next) { if (next.isAuthenticated && !prev.isAuthenticated) { db.connect(connector: MyConnector()); } // ... }); ref.onDispose(db.close); return db; } ``` ### Running queries To expose auto-updating query results, use a `StreamProvider` reading the database: ```dart theme={null} final _lists = StreamProvider((ref) async* { final database = await ref.read(powerSyncInstanceProvider.future); yield* database.watch('SELECT * FROM lists'); }); ``` ### Waiting for sync If you were awaiting `waitForFirstSync` before, you can keep doing that: ```dart theme={null} final db = await ref.read(powerSyncInstanceProvider.future); await db.waitForFirstSync(); ``` Alternatively, you can expose the sync status as a provider and use that to determine whether the synchronization has completed: ```dart theme={null} final syncStatus = statefulProvider((ref, change) { final status = Stream.fromFuture(ref.read(powerSyncInstanceProvider.future)) .asyncExpand((db) => db.statusStream); final sub = status.listen(change); ref.onDispose(sub.cancel); return const SyncStatus(); }); @riverpod bool didCompleteSync(Ref ref, [BucketPriority? priority]) { final status = ref.watch(syncStatus); if (priority != null) { return status.statusForPriority(priority).hasSynced ?? false; } else { return status.hasSynced ?? false; } } final class MyWidget extends ConsumerWidget { const MyWidget({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final didSync = ref.watch(didCompleteSyncProvider()); if (!didSync) { return const Text('Busy with sync...'); } // ... content after first sync } } ``` ### Attachment queue If you're using the attachment queue helper to synchronize media assets, you can also wrap that in a provider: ```dart theme={null} @Riverpod(keepAlive: true) Future attachmentQueue(Ref ref) async { final db = await ref.read(powerSyncInstanceProvider.future); final queue = YourAttachmentQueue(db, remoteStorage); await queue.init(); return queue; } ``` Reading and awaiting this provider can then be used to show attachments: ```dart theme={null} final class PhotoWidget extends ConsumerWidget { final TodoItem todo; const PhotoWidget({super.key, required this.todo}); @override Widget build(BuildContext context, WidgetRef ref) { final photoState = ref.watch(_getPhotoStateProvider(todo.photoId)); if (!photoState.hasValue) { return Container(); } final data = photoState.value; if (data == null) { return Container(); } String? filePath = data.photoPath; bool fileIsDownloading = !data.fileExists; bool fileArchived = data.attachment?.state == AttachmentState.archived.index; if (fileArchived) { return Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ const Text("Unavailable"), const SizedBox(height: 8), ], ); } if (fileIsDownloading) { return const Text("Downloading..."); } File imageFile = File(filePath!); int lastModified = imageFile.existsSync() ? imageFile.lastModifiedSync().millisecondsSinceEpoch : 0; Key key = ObjectKey('$filePath:$lastModified'); return Image.file( key: key, imageFile, width: 50, height: 50, ); } } class _ResolvedPhotoState { String? photoPath; bool fileExists; Attachment? attachment; _ResolvedPhotoState( {required this.photoPath, required this.fileExists, this.attachment}); } @riverpod Future<_ResolvedPhotoState> _getPhotoState(Ref ref, String? photoId) async { if (photoId == null) { return _ResolvedPhotoState(photoPath: null, fileExists: false); } final queue = await ref.read(attachmentQueueProvider.future); final photoPath = await queue.getLocalUri('$photoId.jpg'); bool fileExists = await File(photoPath).exists(); final row = await queue.db .getOptional('SELECT * FROM attachments_queue WHERE id = ?', [photoId]); if (row != null) { Attachment attachment = Attachment.fromRow(row); return _ResolvedPhotoState( photoPath: photoPath, fileExists: fileExists, attachment: attachment); } return _ResolvedPhotoState( photoPath: photoPath, fileExists: fileExists, attachment: null); } ``` # Unit Testing Source: https://docs.powersync.com/client-sdk-references/flutter/unit-testing Guidelines for unit testing with PowerSync For unit-testing your projects using PowerSync (e.g. testing whether your queries run as expected) you will need the `powersync-sqlite-core` binary in your project's root directory. 1. Download the PowerSync SQLite binary * Go to the [Releases](https://github.com/powersync-ja/powersync-sqlite-core/releases) for `powersync-sqlite-core`. * Download the binary compatible with your OS. 2. Rename the binary * Rename the binary by removing the architecture suffix. * Example: `powersync_x64.dll` to `powersync.dll` * Example: `libpowersync_aarch64.dylib` to `libpowersync.dylib` * Example: `libpowersync_x64.so` to `libpowersync.so` 3. Place the binary in your project * Move the renamed binary to the root directory of your project. This snippet below is only included as a guide to unit testing in Flutter with PowerSync. For more information refer to the [official Flutter unit testing documentation](https://docs.flutter.dev/cookbook/testing/unit/introduction). ```dart theme={null} import 'dart:io'; import 'package:powersync/powersync.dart'; import 'package:path/path.dart'; const schema = Schema([ Table('customers', [Column.text('name'), Column.text('email')]) ]); late PowerSyncDatabase testDB; String getTestDatabasePath() async { const dbFilename = 'powersync-test.db'; final dir = Directory.current.absolute.path; return join(dir, dbFilename); } Future openTestDatabase() async { testDB = PowerSyncDatabase( schema: schema, path: await getTestDatabasePath(), logger: testLogger, ); await testDB.initialize(); } test('INSERT', () async { await testDB.execute( 'INSERT INTO customers(name, email) VALUES(?, ?)', ['John Doe', 'john@hotmail.com']); final results = await testDB.getAll('SELECT * FROM customers'); expect(results.length, 1); expect(results, ['John Doe', 'john@hotmail.com']); }); ``` #### If you have trouble with loading the extension, confirm the following Ensure that your SQLite3 binary install on your system has extension loading enabled. You can confirm this by doing the following * Run `sqlite3` in your command-line interface. * In the sqlite3 prompt run `PRAGMA compile_options;` * Check the output for the option `ENABLE_LOAD_EXTENSION`. * If you see `ENABLE_LOAD_EXTENSION`, it means extension loading is enabled. If the above steps don't work, you can also confirm if extension loading is enabled by trying to load the extension in your command-line interface. * Run `sqlite3` in your command-line interface. * Run `.load /path/to/file/libpowersync.dylib` (macOS) or `.load /path/to/file/libpowersync.so` (Linux) or `.load /path/to/file/powersync.dll` (Windows). * If this runs without error, then extension loading is enabled. If it fails with an error message about extension loading being disabled, then it’s not enabled in your SQLite installation. If it is not enabled, you will have to download a compiled SQLite binary with extension loading enabled (e.g. using Homebrew) or [compile SQLite](https://www.sqlite.org/howtocompile.html) with extension loading enabled and include it in your project's folder alongside the extension. # Usage Examples Source: https://docs.powersync.com/client-sdk-references/flutter/usage-examples Code snippets and guidelines for common scenarios ## Using transactions to group changes Read and write transactions present a context where multiple changes can be made then finally committed to the DB or rolled back. This ensures that either all the changes get persisted, or no change is made to the DB (in the case of a rollback or exception). The [writeTransaction(callback)](https://pub.dev/documentation/powersync/latest/sqlite_async/SqliteQueries/writeTransaction.html) method combines all writes into a single transaction, only committing to persistent storage once. ```dart theme={null} deleteList(SqliteDatabase db, String id) async { await db.writeTransaction((tx) async { // Delete the main list await tx.execute('DELETE FROM lists WHERE id = ?', [id]); // Delete any children of the list await tx.execute('DELETE FROM todos WHERE list_id = ?', [id]); }); } ``` Also see [readTransaction(callback)](https://pub.dev/documentation/powersync/latest/sqlite_async/SqliteQueries/readTransaction.html) . ## Listen for changes in data Use [watch](https://pub.dev/documentation/powersync/latest/sqlite_async/SqliteQueries/watch.html) to watch for changes to the dependent tables of any SQL query. ```dart theme={null} StreamBuilder( stream: db.watch('SELECT * FROM lists WHERE state = ?', ['pending']), builder: (context, snapshot) { if (snapshot.hasData) { // TODO: implement your own UI here based on the result set return ...; } else { return const Center(child: CircularProgressIndicator()); } }, ) ``` ## Insert, update, and delete data in the local database Use [execute](https://pub.dev/documentation/powersync/latest/powersync/PowerSyncDatabase/execute.html) to run INSERT, UPDATE or DELETE queries. ```dart theme={null} FloatingActionButton( onPressed: () async { await db.execute( 'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)', ['Fred', 'fred@example.org'], ); }, tooltip: '+', child: const Icon(Icons.add), ); ``` ## Send changes in local data to your backend service Override [uploadData](https://pub.dev/documentation/powersync/latest/powersync/PowerSyncBackendConnector/uploadData.html) to send local updates to your backend service. ```dart theme={null} @override Future uploadData(PowerSyncDatabase database) async { final batch = await database.getCrudBatch(); if (batch == null) return; for (var op in batch.crud) { switch (op.op) { case UpdateType.put: // Send the data to your backend service // Replace `_myApi` with your own API client or service await _myApi.put(op.table, op.opData!); break; default: // TODO: implement the other operations (patch, delete) break; } } await batch.complete(); } ``` ## Accessing PowerSync connection status information Use [SyncStatus](https://pub.dev/documentation/powersync/latest/powersync/SyncStatus-class.html) and register an event listener with [statusStream](https://pub.dev/documentation/powersync/latest/powersync/PowerSyncDatabase/statusStream.html) to listen for status changes to your PowerSync instance. ```dart theme={null} class _StatusAppBarState extends State { late SyncStatus _connectionState; StreamSubscription? _syncStatusSubscription; @override void initState() { super.initState(); _connectionState = db.currentStatus; _syncStatusSubscription = db.statusStream.listen((event) { setState(() { _connectionState = db.currentStatus; }); }); } @override void dispose() { super.dispose(); _syncStatusSubscription?.cancel(); } @override Widget build(BuildContext context) { final statusIcon = _getStatusIcon(_connectionState); return AppBar( title: Text(widget.title), actions: [ ... statusIcon ], ); } } Widget _getStatusIcon(SyncStatus status) { if (status.anyError != null) { // The error message is verbose, could be replaced with something // more user-friendly 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) { // The status changes often between downloading, uploading and both, // so we use the same icon for all three 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); } } ``` ## Wait for the initial sync to complete Use the [hasSynced](https://pub.dev/documentation/powersync/latest/powersync/SyncStatus/hasSynced.html) property (available since version 1.5.1 of the SDK) and register a listener to indicate to the user whether the initial sync is in progress. ```dart theme={null} // Example of using hasSynced to show whether the first sync has completed /// Global reference to the database final PowerSyncDatabase db; bool hasSynced = false; StreamSubscription? _syncStatusSubscription; // Use the exposed statusStream Stream watchSyncStatus() { return db.statusStream; } @override void initState() { super.initState(); _syncStatusSubscription = watchSyncStatus.listen((status) { setState(() { hasSynced = status.hasSynced ?? false; }); }); } @override Widget build(BuildContext context) { return Text(hasSynced ? 'Initial sync completed!' : 'Busy with initial sync...'); } // Don't forget to dispose of stream subscriptions when the view is disposed void dispose() { super.dispose(); _syncStatusSubscription?.cancel(); } ``` For async use cases, see the [waitForFirstSync](https://pub.dev/documentation/powersync/latest/powersync/PowerSyncDatabase/waitForFirstSync.html) method which returns a promise that resolves once the first full sync has completed. ## Report sync download progress You can show users a progress bar when data downloads using the `downloadProgress` property from the [SyncStatus](https://pub.dev/documentation/powersync/latest/powersync/SyncStatus/downloadProgress.html) class. `downloadProgress.downloadedFraction` gives you a value from 0.0 to 1.0 representing the total sync progress. This is especially useful for long-running initial syncs. As an example, this widget renders a progress bar when a download is active: ```dart theme={null} import 'package:flutter/material.dart'; import 'package:powersync/powersync.dart' hide Column; class SyncProgressBar extends StatelessWidget { final PowerSyncDatabase db; /// When set, show progress towards the [BucketPriority] instead of towards /// the full sync. final BucketPriority? priority; const SyncProgressBar({ super.key, required this.db, this.priority, }); @override Widget build(BuildContext context) { return StreamBuilder( 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(); } }, ); } } ``` Also see: * [SyncDownloadProgress API](https://pub.dev/documentation/powersync/latest/powersync/SyncDownloadProgress-extension-type.html) * [Demo component](https://github.com/powersync-ja/powersync.dart/blob/main/demos/supabase-todolist/lib/widgets/guard_by_sync.dart) # Introduction Source: https://docs.powersync.com/client-sdk-references/introduction PowerSync supports multiple client-side frameworks with official SDKs Select your client framework for the full SDK reference, getting started instructions and example code: # JavaScript Web Source: https://docs.powersync.com/client-sdk-references/javascript-web Full SDK reference for using PowerSync in JavaScript Web clients This SDK is distributed via NPM Refer to packages/web in the `powersync-js` repo on GitHub Full API reference for the SDK Gallery of example projects/demo apps built with JavaScript Web stacks and PowerSync Changelog for the SDK ### Quickstart