# PowerSync — full text Every blog post and legal page on https://powersync.com, concatenated. Individual documents are served as markdown at the same URL with a `.md` suffix (for example https://powersync.com/blog/.md). # Introducing Checkpoint Requests > Checkpoint Requests answer: has the local database caught up to the source database right now? This lets your app wait for the data it needs before proceeding. - Published: 2026-09-09 - Author: Steven Ontong - Category: Engineering - Source: https://powersync.com/blog/checkpoint-requests-client-synced-now --- ## Background PowerSync partially syncs your source database to a local in-app SQLite database. Partial sync is defined by a Sync Config: a YAML configuration containing Sync Stream definitions with SQL-like parameterized queries that describe the data to sync to each user. The PowerSync Service replicates relevant data from the source database, based on the Sync Config, into precomputed "buckets". Connecting clients receive bucket operations through a stream connection organized around **checkpoints**. A checkpoint usually maps to a set of complete source database transactions. A transaction's operations will never be split between checkpoints. The PowerSync Service and client SDK automatically synchronize the local SQLite with the source database. Developers can generally rely on local state being applied consistently at the next safe checkpoint. Reactive queries typically automatically update in-memory state when local SQLite changes are made. This is convenient for reacting to changes from both local (the current user) and externally sourced operations. For externally sourced operations, PowerSync guarantees that those remote changes would be applied at *some* point in the future, but historically there wasn't an easy way to definitively answer the question of "is the client fully synced *now*?" In the next section we explore this problem in a bit more detail. ## Wait for first sync PowerSync has historically exposed indicators for when the **first complete sync** has finished (to a freshly-initialized SQLite database). The client-side PowerSync database exposes a global `waitForFirstSync` method which resolves once the first complete sync has been applied locally. ```javascript // App initialization code await powerSync.waitForFirstSync() // App now knows the initial completely-synced data is available in SQLite // Queries should now reflect the user's data at this point in time ``` We also expose the first sync's completion on a per-stream basis: ```javascript const streamSubscription = await db .syncStream("todos", { list_id: "abc123" }) .subscribe(); await streamSubscription.waitForFirstSync(); await streamSubscription.waitForFirstSync(); ``` Those APIs are great for the specific use case of waiting on the *first complete sync*. However, what we've found from developer feedback is that apps often need to ensure that the local database has been synced *again* after the first complete sync. For example, consider this flow: - A user uses a PowerSync-enabled app. - App code which relies on `waitForFirstSync` gates certain logic, delaying until the sync completes. - The user closes the app. - The source database now contains changes which need to be synced to the user. - The user re-opens the app. - The first sync has already completed, so `waitForFirstSync` resolves. - The user has pending operations to sync, but the app has no clean way of waiting for those changes to be applied. - The app typically routes the user to specific views. For some workflows, the above is fine. Reactive queries typically update with the latest data once the incoming changes have been synced. For some other workflows, an application might **need** the latest data to be synced before allowing the user to proceed. A classic example of this could be push notifications. Let's say the application backend committed a change to the source database, which triggered a push notification to the device. The user tapped the notification related to the specific data, opening the application. App logic most likely will need the corresponding data to be queryable once handling the notification tap. Developers often find themselves needing an additional gating mechanism: waiting for the **next** checkpoint to be synced. ## Checkpoints ### (Legacy) Write Checkpoints An internal mechanism called Write Checkpoints is what the PowerSync client SDKs use to ensure data consistency. The general rule for consistency in PowerSync is: > An SDK client won't apply incoming sync operations until all its local mutations have been completed **and** the source database operations related to those mutations have been received back through the latest checkpoint. This [guarantees](https://docs.powersync.com/architecture/consistency#powersync-designed-for-causal+-consistency) causal consistency. The only exception to this is if [sync priority 0](https://docs.powersync.com/sync/advanced/prioritized-sync#special-case-priority-0) is used, but we'll ignore that for this explanation. The rule above is quite a mouthful, so we can break it down into an example. As a reminder, the PowerSync Service consumes a CDC replication stream from the source database and replicates its operations into precomputed [bucket storage](https://docs.powersync.com/architecture/powersync-service#bucket-storage). Consuming this stream requires some form of positional information. A typical mutation flows through PowerSync as follows: 1. The app makes the mutation via a SQLite query, e.g. `INSERT INTO my_table(...) VALUES (...)`. 2. The PowerSync client SDK queues this operation and other transaction operations in the upload queue. 3. The PowerSync client SDK calls the `BackendConnector` `uploadData` method at appropriate times, allowing the application code to fetch `CrudTransaction`s or `CrudBatch`es. - The connector passes the mutations to the application backend, where the application backend may modify, persist or deny those mutations from entering the source database. - The `.complete()` method is called on the `CrudTransaction` or `CrudBatch`. This removes the entries from the upload queue. 4. Once the upload queue is empty, the PowerSync client SDK requests a Write Checkpoint from the PowerSync Service. - The PowerSync Service associates the current source database replication position with the user and client that requested the Write Checkpoint. - The PowerSync Service returns a marker to the client, which is a target the SDK uses to wait for the corresponding checkpoint to be received from the PowerSync Service. The SDK won't apply any incoming operations to the local SQLite tables until it has fully received the corresponding checkpoint. You might ask yourself, why are we talking about uploads when we want to know when the PowerSync client SDK has synced a checkpoint? The reason for all that context is that historic Write Checkpoints have been answering that question all along. It's just been an internal facet of PowerSync. Write Checkpoints have allowed the client SDK to wait for a specific source database state to be present locally; we'd need something similar if we'd like to wait for subsequent checkpoint syncs. ### Checkpoint Requests We recently released the Checkpoint Request API across all SDKs, requiring PowerSync Service version 1.24.0 or later. It allows developers to answer the question, "Has the application synced all relevant changes in the source database up to now?" Checkpoint Requests use the PowerSync Service to make an association with the source database's current replication position. This association is tracked on the client and service side. The PowerSync Service reports to the SDK once the data related to the association has been sent to the client. The client can then notify application code once the Checkpoint Request has been applied. After it's applied, the local SQLite tables will contain all relevant state corresponding to the source database at the time of the association. Checkpoint Requests require a connected or connecting PowerSync SDK client. A request cannot be created while the client is fully disconnected. While the client is connecting, request creation waits until the PowerSync Service is reachable. If the connection drops while waiting for a created request, the app can reconnect and wait on the same request again. ```javascript // Make sure to connect using the new `checkpointMode` await db.connect(connector, { checkpointMode: 'requests' }); // Perform some operation // ... // Create a checkpoint request const checkpoint = await db.requestCheckpoint(); // Wait for the checkpoint to sync, with a 30-second timeout await checkpoint.waitForSync({ signal: AbortSignal.timeout(30_000) }); // Now the data is available in the local SQLite database ``` > **Note** > > **Note:** Checkpoint Requests are currently in alpha. Write Checkpoints still exist, but in the future we will completely replace legacy Write Checkpoints with Checkpoint Requests. ## Use cases ### Waiting for the next sync For the notification example mentioned, an app can now create a Checkpoint Request and wait for it to be synced when handling the notification app-open event: ```javascript async function handleNotification() { // ... logic const checkpoint = await db.requestCheckpoint(); await checkpoint.waitForSync({ signal: AbortSignal.timeout(30_000) }); // The app can now query for data related to the notification event // and navigate to specific views if necessary. } ``` ### How we use Checkpoint Requests in the Dashboard We're using Checkpoint Requests in the PowerSync Dashboard itself to handle a subtle timing issue: a management API call could succeed before the resulting change reached the browser's local database. After creating an instance, for example, the dashboard could try to open its page before its data was available locally. Requesting a checkpoint after the API call lets us wait for local sync to catch up before navigating. ### Waiting for writes The PowerSync client SDK uses the same mechanism for Checkpoint Requests as it does for the historic Write Checkpoint gates mentioned above. This means that the existing consistency gates and Checkpoint Requests are inherently linked. For Write Checkpoints: > An SDK client won't apply incoming sync operations until all its local mutations have been completed **and** the source database operations related to those mutations have been received back through the latest checkpoint. This ties Checkpoint Requests and local mutations. A user can now write code like this: ```javascript async function someMutation() { // Perform some local writes await db.execute( `INSERT INTO ${TODOS_TABLE} (id, created_at, created_by, description, list_id, completed) VALUES (uuid(), datetime(), ?, ?, ?, ?)`, [ connector.currentUserID, todo.description, listId, todo.isComplete ? 1 : 0 ] ); // This queues the local write for asynchronous upload. // Create a checkpoint request and wait for it to sync. const checkpoint = await db.requestCheckpoint(); await checkpoint.waitForSync({ signal: AbortSignal.timeout(30_000) }); // The PowerSync client SDK and Service associate the request with the // current database replication position and wait for that data to be // synced and applied locally. // // The consistency barriers also ensure that the wait completes only // after the local write has been uploaded and the corresponding // checkpoint has been synced and applied locally. // // The application backend has now processed the local write above, // and the result of that processing is available locally. // We can query the database for any additional changes the backend // made when creating the TODO record. } ``` The code above is possible due to our gating logic for applying incoming changes while local mutations are in the upload queue. Essentially, we perform a local mutation which queues the item for upload. We also create a Checkpoint Request after the mutation. If that explicit request is created while the upload is pending, the SDK creates a newer internal request after the upload queue empties. We'll only get a resolve once the effective checkpoint has been synced. **Note:** The exact request ordering is not deterministic. The upload may complete and create its internal Checkpoint Request before the explicit `db.requestCheckpoint()` call. In either order, the checkpoint gate remains valid and the wait only completes after the relevant upload state and requested source database state have been applied locally. This assumes that `uploadData()` returns only after the application backend has committed the uploaded changes to the source database. Application backends that process uploads asynchronously should use Custom Checkpoint Request handlers by implementing `CustomCheckpointRequestConnector`, so the checkpoint is created only after the queued work has been processed. See additional details in our [documentation](https://docs.powersync.com/client-sdks/advanced/checkpoint-requests#asynchronous-upload-backends). This effectively provides a means for waiting for the upload queue to drain and the next checkpoint to be synced. ### Intermittent sync Now that we can wait for the next sync, we can temporarily connect a client to PowerSync, wait until the requested checkpoint has been fully applied, then disconnect. This could be useful in cases where apps don't need to be permanently connected to the PowerSync Service. ```javascript async function sync() { await db.connect(connector, { checkpointMode: 'requests' }); const checkpoint = await db.requestCheckpoint(); await checkpoint.waitForSync({ signal: AbortSignal.timeout(30000) }); // No need to stay connected after we've got the latest data await db.disconnect(); } ``` ## Conclusion PowerSync already keeps the local SQLite database continuously and reactively in sync. Checkpoint Requests add an explicit synchronization boundary: an application can ask the PowerSync Service to capture the source database's current replication position, then wait until all relevant data through that position has been applied locally. `waitForFirstSync` answers, "Has this database completed its initial sync?" A Checkpoint Request answers, "Has this database caught up to the relevant source database state *now*?" That distinction makes flows such as notification handling, explicit refreshes, write-dependent actions and intermittent sync easier to coordinate without polling or relying on arbitrary delays. In short, Checkpoint Requests turn an eventual catch-up into an observable application event. When an app needs to know that relevant source data is locally available before it proceeds, it can now wait for that point directly. --- # Offline-First at National Scale > Four public-sector deployments — a national school meals program, a government retail network, a telecoms field workforce and a social housing repairs operation — and the shared constraints behind them: no network, in-country data, and scale proven before development starts. - Published: 2026-08-17 - Author: Conrad Hofmeyr - Category: Showcase - Source: https://powersync.com/blog/offline-first-at-national-scale --- A field test of PowerSync for a government project last year covered five schools in a mountainous region in Asia. One school had around 20 Mbps of connectivity. Two ran between zero and one megabit. Two had no coverage at all, and in those the procedure was for a staff member to travel to a location with signal roughly once every ten days and sync from there. Guaranteed local retention for extended offline durations (weeks) was baked into the requirements, so that a missed trip would not cost anyone their records. Testing ran on low-end Android handsets and on recent iPhones, and the application performed well across all five sites. Requirements like that are ordinary in the public sector. Connectivity is rarely negotiable, because a government application has to cover everyone within a jurisdiction, including the parts of it where the network has not been built. In-country data residency is non-negotiable. And the user count is derived from the size of the population being served, which means it is known before development starts and scalability has to be demonstrated early on during evaluation. ## PowerSync Public-Sector Deployment Examples Here are a few anonymized representative examples of PowerSync public-sector deployments: ### A National School Program, Asia A national education authority is implementing a PowerSync-enabled software application that records daily attendance and meal distribution at schools. The application aggregates those figures up through a regional hierarchy all the way to the national level. It is built in Flutter and runs on Android and iOS devices already owned by school staff. The upcoming national rollout will involve hundreds of millions of records captured per day across millions of students and thousands of schools. The team initially intended for senior administrators to sync a single summary table of roughly two million rows, with a target initial sync under two minutes. That target was not achievable at that row count with PowerSync. The workable approach was restructuring: separate aggregated tables per administrative level, so an administrator syncs a few dozen rows per day and a regional administrator syncs under a thousand. The data volume on the wire dropped by three orders of magnitude. The scale of this project breaks assumptions outside the sync layer as well. The team had been using a commercial hosted identity provider and dropped it once the per-user pricing was extrapolated to a national rollout, replacing it with their own JWT signing. PowerSync is not an identity provider. It verifies JWTs from whatever mints them, which means the identity system each organization already runs. Accuracy is what a program like this gets judged on, and that is the citizen-facing outcome here. A meal served in a school without connectivity is recorded at the point of service, and it reaches the government as captured data instead of being reconstructed from paper or memory some weeks later. Parents will eventually get a view of the same records. A parent account will only sync its own children's data, enough to show whether a child was marked present and received a meal on a given day. Parent logins are scheduled for a release after the school-level features that make up the first rollout, and they change the shape of the load considerably. Parent sessions are short, a few minutes each, and they cluster almost entirely between the start of the school day and mid-morning, so the deployment has to absorb millions of brief connections inside a few hours. ### Nationwide Government Retail Locations, Asia A technology partner is building an offline-first point-of-sale system for a government program that involves tens of thousands of retail locations across the country. The software is built in Flutter and runs on low-cost Android point-of-sale terminals. Connectivity at these sites is intermittent, often for hours at a time, and a point-of-sale system that depends on a live server connection stops operating during those windows. With PowerSync, transactions complete on the terminal and reconcile when the link returns, so retail locations keep trading and citizens are served. The deployment is self-hosted on managed Kubernetes, with Postgres as both the source database and the bucket storage. ### Field Technicians, Asia-Pacific A government-owned telecommunications infrastructure operator is building a native iOS application for its field workforce, numbering in the thousands of technicians. The previous application was a PWA. It handled offline conditions poorly, and iOS updates periodically broke the runtime beneath it. The replacement is written in Swift and SwiftUI, with PowerSync self-hosted inside the operator's own cloud environment to satisfy in-country data residency requirements. Work orders, calendars, messages and notifications sync to the device. Writes are uploaded through the operator's existing API gateway and backend services, which allows the existing authentication and security model to remain unchanged. For households waiting on a connection or a repair, the effect is fewer repeat visits. The technician arrives with the complete job record regardless of coverage at the site and can close the work on the first appointment. ### Social Housing Maintenance, Europe The same pattern turns up again in a housing and care provider that maintains tens of thousands of homes on behalf of local and central government. Its repairs and maintenance application runs on thousands of devices and several of its contracts carry data sovereignty terms that require hosting within the country. Repairs are recorded at the property, including inside buildings with no reception, which keeps tenant repair histories accurate and complete. ## Security Review and Continuity Risk In a public-sector deal, the information security review usually asks for things like a SOC 2 report, third-party penetration test results, static analysis and dependency scan output, and a vendor willing to work through a long security questionnaire. A harder question that often comes up is what happens if the vendor discontinues the product. That is a fair question to put to us, and two of the four customers above are here because it happened to them: both were running MongoDB Atlas Device Sync when it was deprecated. PowerSync inherently provides continuity guarantees. The PowerSync client SDKs are Apache 2.0 licensed. The PowerSync Service is available as Open Edition under the Functional Source License, which converts to Apache 2.0 over time, so a self-hosted deployment does not depend on our continued existence to keep running. ## Resilience on the Device PowerSync is designed to support devices offline for extended periods — weeks at a time or even longer. It is also built to assume unreliable client-side storage. PowerSync maintains checksums per bucket of data on both the client and the server, and validates them as part of every checkpoint. A mismatch should never happen during normal operation, but local storage on inexpensive Android hardware does fail, and processes get killed mid-write. When a bucket checksum does not match, PowerSync re-downloads that bucket automatically. ## Running the Service A single PowerSync instance supports on the order of 50,000 to 100,000 concurrently connected clients depending on the shape and volume of the synced data, and deployments beyond that use a sharded architecture. All four example deployments above run PowerSync self-hosted, in each case for residency or sovereignty reasons. It is the same service image with the same sync semantics as PowerSync Cloud, configured through YAML files or admin APIs rather than a dashboard, and it exposes Prometheus metrics and a diagnostics endpoint for monitoring in Grafana or an equivalent. Teams that already run their own infrastructure tend to want their sync layer visible in the same dashboards as everything else. ## What These Deployments Have in Common A school meals program, a government retail network, a telecoms field workforce and a social housing repairs operation have very little to do with one another, but their requirements are similar nevertheless. Each one needed the application to keep working with no network, the data to stay inside the country, and scalability and reliability to be demonstrated early on. If you are scoping something with these characteristics, our team is available to work through sizing and architecture before you commit to a design. --- # Migrate from Electric Cloud to PowerSync Cloud > Use this guide to migrate from Electric Cloud to PowerSync Cloud. It covers high-level concepts to help you understand PowerSync, and covers the migration step-by-step. - Published: 2026-08-12 - Author: Kobie Botha - Category: Tutorial - Source: https://powersync.com/blog/migrate-from-electric-cloud-to-powersync-cloud --- Electric [announced](https://electric.ax/blog/2026/08/11/electric-joining-databricks) that it is joining Databricks and Electric Cloud is shutting down. Their official guidance is to move to either self-hosted Electric or another provider. This guide is for users looking to migrate to an alternative cloud provider of a Postgres-backed sync engine, namely PowerSync. This is an implementation guide for teams already using Electric Cloud's current Postgres Sync service. It assumes Postgres remains the system of record and covers high-level concepts to assist with a migration. ## Concepts PowerSync Sync Streams are analogous to Electric Shapes, not Electric Streams. Electric Streams is a separate product and is out of scope for this guide. While Electric Shapes and PowerSync Sync Streams both control partial sync, there are significant differences meaning that Shape to Sync Stream migration will most likely not be 1-for-1, and re-designing partial sync will most likely be required. A key architectural difference between Electric Shapes and Sync Streams is that Shapes are created on demand from the client, per request, while Sync Streams are declared as named queries ahead of time that clients then dynamically subscribe to when needed, each subscription passing its own parameters. This architecture drives how authorization, offline behavior, and writes work. The table below summarizes the specific differences between Electric Shapes and Sync Streams across these and other key dimensions: | Dimension | Electric (Shapes) | PowerSync (Sync Streams) | | --- | --- | --- | | Definition | Client-side, per request. Using `table` and optional `where`, `columns`, and `queryable_columns` parameters. Each parameter set identifies a distinct Shape. | Server-side, names SQL queries in a deployed YAML config. Clients subscribe by name and supply parameters; `auto_subscribe` starts a stream automatically. | | Relational scope | Single table only. Use subqueries to filter, or multiple Shapes to sync related data. | `JOIN` supported. One stream can carry multiple queries. | | Filter language | Supported PostgreSQL `WHERE` expressions, positional parameters, arrays, and subqueries. No non-deterministic functions. [[ref](https://electric.ax/docs/sync/guides/shapes#supported-operators)] | SQL subset with inner joins, subqueries, CTEs, and transformations; no `GROUP BY`, `ORDER BY`, `LIMIT`, or `UNION`. No non-deterministic functions. [[ref](https://docs.powersync.com/sync/supported-sql)] | | Authorization model | "Production apps should request shapes through your backend API for authorization and security". [[ref](https://electric.ax/docs/sync/guides/shapes#defining-shapes)] | Sync Stream queries define download access using signed JWT claims. Treat client-provided subscription parameters as untrusted. | | Client store | In-memory rows by default. Persistence and offline behavior require a separate client store or integration, such as PGlite or TanStack DB collections. | SQLite with a declared `AppSchema`. The Web SDK uses persistent IndexedDB by default; additional VFSes are available, and an in-memory VFS is optional. | | Local queries | `shape.rows`, `useShape`, TanStack DB live queries. | SQL against SQLite (`useQuery`, `watch()`). Also supports the TanStack DB interface and SQLite ORMs like Drizzle. | | Writes | Electric provides read-path sync only. The app defines its write path. | Local `INSERT`, `UPDATE` and `DELETE` operations enter a FIFO upload queue. Developer-defined `uploadData()` applies mutations via the developers' existing backend which authorizes and applies them. | | Primary keys and types | Must always include Postgres primary key columns. | Each synced table requires a single unique text `id` column. Columns can be aliased, concatenated or casted [[ref](https://docs.powersync.com/sync/advanced/client-id#client-id)]. The PowerSync TanStack DB integration provides automatic support for rich types. [[ref](https://tanstack.com/db/latest/docs/collections/powersync-collection)] | | Performance consideration | Shape filter design affects throughput. [[ref](https://electric.ax/docs/sync/guides/shapes#throughput)] | Bucket cardinality affects sync performance. [[ref](https://docs.powersync.com/sync/rules/organize-data-into-buckets#limit-on-number-of-buckets-per-client)] | ## Migration Note that both Electric Sync and PowerSync codebases are public, so agents should generally not struggle with implementing migrations. Also see the PowerSync [agent resources](https://docs.powersync.com/tools/ai-tools). If you need assistance, join the [PowerSync Discord](https://discord.gg/powersync). ### Overview of Migration Steps 1. [Configure Postgres](#configure-postgres) 2. [Connect PowerSync](#connect-powersync-to-postgres) 3. [Define and test Sync Streams](#define-and-test-sync-streams) 4. [Set up Authentication](#set-up-authentication) 5. [Migrate the frontend code](#migrate-the-frontend-code) ### Configure Postgres 1. Create a Postgres role and publication as per [documentation](https://docs.powersync.com/configuration/source-db/setup#supabase). 1. Note: it is not possible to reuse the Electric publication, since PowerSync requires a publication named `powersync`. 2. Note that `BYPASSRLS` is commonly used for the PowerSync role. This is because Sync Stream definitions enforce authorization. ### Connect PowerSync to Postgres 1. Connect your PowerSync instance to your Postgres environment 1. Dashboard [instructions](https://docs.powersync.com/configuration/source-db/connection#supabase) 2. CLI [instructions](https://docs.powersync.com/tools/cli#cloud-workflows) 2. Since the PowerSync Service connects directly to Postgres, various network-level security mechanisms are [supported](https://docs.powersync.com/configuration/source-db/security-and-ip-filtering#security-and-ip-filtering). ### Define and Test Sync Streams In this step you will write your Sync Streams YAML. This will: - Define how subsets of your Postgres data are synced to SQLite (on the client) - Move download authorization into Sync Stream queries. You keep write authorization in your backend. PowerSync uses signed JWT claims for access checks. Sync Streams accept client parameters, but these should not be relied on for authorization checks. - Refer to the Sync Streams [documentation](https://docs.powersync.com/sync/streams/overview) - Note that if the [supported Sync Streams SQL](https://docs.powersync.com/sync/supported-sql) doesn't support the specific query you are trying to write, sync the required streams and then run `ORDER BY`, `LIMIT`, aggregates, joins, etc. in local SQLite. #### Example A Shape that syncs a user's projects as follows: ```typescript const { data } = useShape({ url: `http://localhost:3000/v1/shape`, params: { table: `projects`, where: `owner_id = ${currentSession().userId}`, }, }) ``` Can be defined in Sync Streams as this `my_projects` stream: ```yaml streams: my_projects: query: SELECT * FROM projects WHERE owner_id = auth.user_id() ``` You can then define a separate Sync Stream to sync each project's tasks (`project_tasks` stream), where the client provides the `project_id`: ```yaml streams: my_projects: ... project_tasks: query: | SELECT * FROM tasks WHERE project_id = subscription.parameter('project_id') AND project_id IN (SELECT id FROM projects WHERE owner_id = auth.user_id()) ``` #### Run a Sync Test from the PowerSync Dashboard Once you've defined your Sync Streams, you can run a Sync Test in the PowerSync Dashboard to validate that data is syncing to the client as expected. ### Set Up Authentication Electric relies on a backend to validate requests to Shapes. With PowerSync that logic is contained in your Sync Stream queries, and clients are then able to connect to the PowerSync Service with a JWT minted from your backend, instead of using the backend as a step in the middle. `auth.user_id` illustrates this in the Sync Streams example from above: ```yaml query: SELECT * FROM projects WHERE owner_id = auth.user_id() ``` Our [authentication documentation](https://docs.powersync.com/configuration/auth/overview) covers how to set this up. ### Migrate the Frontend Code The PowerSync client owns a local SQLite database that intelligently merges rows from all active Sync Streams. It exposes local SQL and live query (watch) APIs. It optionally records local writes into a FIFO upload queue. Note that migrations from PGlite to SQLite are possible but out of scope for this guide. PowerSync provides SDKs for many platforms: Swift, Kotlin, Dart, .NET, Rust and JS/TS (Web, React Native, Node, Capacitor and Tauri). This section only covers Web JS/TS. 1. [Install](https://docs.powersync.com/client-sdks/reference/javascript-web#installation) the PowerSync Web SDK with `pnpm install @powersync/web` 2. Use the PowerSync Dashboard or CLI to generate the client-side SQLite schema ([docs](https://docs.powersync.com/client-sdks/reference/javascript-web#1-define-the-client-side-schema)). Note that an `id` column is added automatically. It will look similar to this: ```typescript import { column, Schema, Table } from '@powersync/web' const projects = new Table({ name: column.text, owner_id: column.text, }) const tasks = new Table({ project_id: column.text, title: column.text, status: column.text, }, { indexes: { by_project: ['project_id'] } }) export const AppSchema = new Schema({ projects, tasks }) ``` 3. [Instantiate](https://docs.powersync.com/client-sdks/reference/javascript-web#2-instantiate-the-powersync-database) the PowerSyncDatabase. Note that you must only create one `PowerSyncDatabase` instance for each database file ```typescript import { PowerSyncDatabase } from '@powersync/web' export const db = new PowerSyncDatabase({ schema: AppSchema, database: { dbFilename: 'powersync-v1.db' }, }) ``` 4. The Web SDK uses a persistent IndexedDB VFS by default. [Select another VFS](https://docs.powersync.com/client-sdks/reference/javascript-web#sqlite-virtual-file-systems) if you have specific browser, performance, or multi-tab requirements. 5. [Integrate](https://docs.powersync.com/client-sdks/reference/javascript-web#3-integrate-with-your-backend) with your backend. This requires implementing two methods for `PowerSyncBackendConnector`: `fetchCredentials` and `uploadData` 1. `fetchCredentials()` returns the JWT used to authenticate against PowerSync with and download synced data, as well as the PowerSync Cloud endpoint 2. `uploadData()` defines how local mutations are sent to your backend API - you should be able to re-use the existing backend you have in place today 6. Replace client-side queries. This is where the bulk of the work will take place, but agents should be pretty good at it. Below are some more tips to get you going: 1. Replace shape [handle/offset usage](https://electric.ax/docs/sync/guides/shapes#subscribing-to-shapes) with [`waitForFirstSync()`](https://docs.powersync.com/client-sdks/usage-examples#wait-for-the-initial-sync-to-complete) and Sync Stream [status checks](https://docs.powersync.com/sync/streams/client-usage#checking-sync-status). 2. Tie on-demand Sync Stream subscriptions to component or route lifetime. The PowerSync React Hooks can [automatically subscribe/unsubscribe](https://docs.powersync.com/sync/streams/client-usage#framework-integrations). 3. Note that when subscribing to a Sync Stream on the client, the TTL can be overridden. The default is 24 hours: a shorter TTL reduces disk usage, a longer TTL improves page reload performance. 4. When logging the user out or switching accounts, only use `disconnectAndClear()` once the upload queue has been emptied, otherwise local mutations will get discarded. 7. Implement writes 1. Electric is read-path only, so this guide doesn't cover migrating a write path. However, it's highly recommended to use the PowerSync upload queue to ensure consistency instead of sending mutations directly to your backend. You might notice data flicker on the client if you write directly to your backend APIs / bypass the PowerSync upload queue. 2. Follow [this guide](https://docs.powersync.com/configuration/app-backend/client-side-integration) for integrating the PowerSync upload queue with your backend APIs --- # PowerSync Changelog: June & July 2026 > We released v2.0 of our JavaScript SDKs, added support for Azure DocumentDB and Convex as source databases, shipped an official Terraform provider, and merged some of the most substantial community contributions we've had yet. - Published: 2026-08-11 - Author: Kobie Botha - Category: Product Update - Source: https://powersync.com/blog/powersync-changelog-june-july-2026 --- In the last edition I wrote about overhauling and shipping v2.0 of our Dart SDK. It's important to us that PowerSync is well-maintained, so this time we extended that effort to our JavaScript SDKs. We tackled a long laundry list of DX issues and released v2.0 of our web, React Native, Capacitor and Node.js SDKs. More on that in the feature story below. Beyond that, we added support for new source databases (Azure DocumentDB and Convex), released an official Terraform provider, and shipped some of the most substantial community contributions we've had yet. Community members adding non-trivial features is becoming a trend, and we love it. ## Product updates shipped **PowerSync Service:** * **Azure DocumentDB as a source database**: DocumentDB (formerly Azure Cosmos DB for MongoDB vCore) now works through our existing MongoDB connector: link your connection string and PowerSync detects it automatically. [Release notes](https://releases.powersync.com/announcements/announcing-azure-documentdb-support-experimental). * **Convex as a source database**: Building with Convex and need offline support? You can now connect your Convex database to PowerSync. Writes still go through your existing Convex mutations, and each client only syncs the data it needs. It's an experimental release, and your feedback shapes where we go from here. [Release notes](https://releases.powersync.com/announcements/announcing-convex-backend-support-experimental) and [design notes](https://powersync.com/blog/convex-powersync-design-notes). * **Sync Streams hardening**: The compiler now warns about queries that can silently sync rows under the wrong name (unquoted JOIN aliases, `SELECT *` after an aliased column). We also fixed several cases where our evaluator diverged from SQLite semantics (thanks @sravan27!). [Changelog](https://releases.powersync.com/announcements/powersync-service). **Client SDKs:** * **JavaScript SDKs v2.0**: Focused on a more stable and intuitive public API, and simpler setup. See the feature story below or review the [release notes](https://releases.powersync.com/announcements/v2-0-of-powersync-javascript-sdks) for a complete summary and how to handle breaking changes. * **Multi-process SQLite access (Swift v1.15.0)**: App Extensions and App Groups are now supported: the database can live in an App Group container, with the SDK coordinating access between your app and its widgets or extensions. This is an early release with known footguns. We have an intuition that better DX is possible here, but we need your feedback to shape this. [Release notes](https://releases.powersync.com/announcements/powersync-swift-sdk) and [Docs](https://docs.powersync.com/client-sdks/reference/swift#app-groups-and-app-extensions). * **In-memory databases for the web (v1.39.0)**: `WASQLiteVFS.InMemoryVfs` keeps the whole database in memory with no persistence, which is useful for local development and online apps. [Docs](https://docs.powersync.com/client-sdks/reference/javascript-web#3-in-memory-vfs). * **Bring your own HTTP client (Dart/Flutter v2.3.0)**: `SyncOptions.httpClient` lets you add headers, trust self-signed certs, or swap in a faster client like `cronet_http`. SQLCipher is also back as a native encryption option. [Docs](https://docs.powersync.com/client-sdks/reference/flutter#custom-http-clients-and-headers). * **Room 3.0 (Kotlin v1.14.1)**: Our Room integration for Kotlin now uses Room 3.0. This is a breaking change: update your project to Room 3.0 when upgrading to v1.14.1. [Docs](https://docs.powersync.com/client-sdks/orms/kotlin/room). **Tools:** * **Official Terraform provider**: Manage PowerSync Cloud projects and instances as Infrastructure-as-Code. [Release notes](https://releases.powersync.com/announcements/an-official-terraform-provider-for-powersync-1). * **Custom roles and permissions (Dashboard)**: Team and Enterprise customers can now create and assign custom Dashboard roles themselves, with project and instance scoping and fine-grained permissions. Previously this had to be configured by our team. [Docs](https://docs.powersync.com/tools/powersync-dashboard#roles-and-permissions). * **More ways to search instance logs (Dashboard)**: Building on May's [searchable logs release](https://releases.powersync.com/announcements/searchable-instance-logs-in-the-powersync-dashboard), we added additional quality-of-life improvements such as excluding terms with a `-` prefix and clicking a timestamp to set the range start or end. [Docs](https://docs.powersync.com/maintenance-ops/monitoring-and-alerting#instance-logs). * **Smarter CLI commands (v0.10.0)**: Commands that take `--instance-id` now resolve the project and org from it automatically (previously you had to add these as flags yourself). [Docs](https://docs.powersync.com/tools/cli). **New demos & guides:** * [It's easier than ever to add your own source DB connector](https://github.com/powersync-ja/powersync-service/tree/main/docs/replication) to PowerSync. Steven documented how replication in the PowerSync Service works — knowledge that until now mainly lived in people's heads. * [Supported platforms reference](https://docs.powersync.com/resources/supported-platforms) — we fleshed out this page to include more detail, like minimum platform versions per SDK, so you can quickly check whether PowerSync runs on your target platform. * [.NET attachments demo](https://github.com/powersync-ja/powersync-dotnet/tree/main/demos/MAUITodo) — the MAUI To-Do demo now also shows offline-first file syncing with the attachment APIs from v0.1.2. * [Load custom SQLite extensions](https://docs.powersync.com/client-sdks/advanced/sqlite-extensions) — how to load custom SQLite extensions into the client-side database. ## Community feed * Conrad wrote about how we built our internal company brain: data from 14+ systems aggregated into Postgres, synced to local SQLite with PowerSync, and queried by agents from Slack, a CLI, or fully offline with local models. [Read the post](https://powersync.com/blog/building-our-company-brain-agents-sqlite-offline-capable). * Manrich shared the dogfooding story of rebuilding the PowerSync Dashboard on our own sync engine. [Read the post](https://powersync.com/blog/rebuilding-powersync-dashboard). * Dean, Christiaan, Steven, and Simon manned our booth at Local-First Conf in Berlin. Thanks to everyone who came to chat! [Talks are on YouTube](https://www.youtube.com/playlist?list=PLXxEKA_dxoH0). * [@sravan27](https://github.com/sravan27) fixed several cases where Sync Streams evaluation diverged from SQLite semantics, which could silently sync the wrong rows ([json_each](https://github.com/powersync-ja/powersync-service/pull/647), [division by zero](https://github.com/powersync-ja/powersync-service/pull/646), [signed casts](https://github.com/powersync-ja/powersync-service/pull/645), [alias warnings](https://github.com/powersync-ja/powersync-service/pull/662)). Disclosure: most of this came out of a paid hardening sprint we ran with Sravan. Thank you for the solid work. * [@asiergmorato](https://github.com/asiergmorato) added multi-process database support to the Swift SDK, which became the App Groups feature ([PR #147](https://github.com/powersync-ja/powersync-swift/pull/147)). He needed it for his own app, [Fitwoody](https://fitwoody.camp/). Also thank you for your [Swift Data integration](https://github.com/powersync-community/swift-data), which lets Swift Data models persist and sync through PowerSync. * [@mandrade2](https://github.com/mandrade2) tried out [our Tauri integration tests](https://powersync.com/blog/tauri-integration-tests), hit an error running them in Vitest's iframe, fixed it in [PR #991](https://github.com/powersync-ja/powersync-js/pull/991), and we've since updated the post with his fix. We love this kind of feedback loop, thank you! * Thank you also to [@henriquekraemer](https://github.com/powersync-ja/powersync-service/pull/734), [@marioortizmanero](https://github.com/powersync-ja/powersync-kotlin/pull/357), [@rumitvn](https://github.com/powersync-ja/powersync.dart/pull/432), [@delagen](https://github.com/powersync-ja/powersync-js/pull/1016), [@sincraianul](https://github.com/powersync-ja/powersync-js/pull/1051), [@daniel-vacic](https://github.com/powersync-ja/powersync-native/pull/23), [@Ishant5436](https://github.com/powersync-ja/powersync-cli/pull/56), [@shenlong-tanwen](https://github.com/powersync-ja/sqlite_async.dart/pull/145), and [@VIVAAN-DHAWAN](https://github.com/powersync-ja/powersync-js/pull/975) for your contributions — we're seeing more and more substantial features and fixes from the community and are loving it. * Yahya ([@whygee-dev](https://github.com/whygee-dev)) built [powersync-mdbx](https://github.com/whygee-dev/powersync-mdbx), a research reimplementation of the PowerSync replication protocol in Rust + MDBX, with promising benchmarks. It's not a drop-in replacement today, but as Ralf put it: "It's a very good observation that the PowerSync model fits quite well into a KV store, and does not need a full database like MongoDB or Postgres." The thread is in [`#engineering-discussions`](https://discord.com/channels/1138230179878154300/1504213613009834145/1527822415516663828). * New proposal: [SQLite as sync bucket storage](https://github.com/powersync-ja/powersync-service/pull/674). Steven is experimenting with SQLite (and MySQL) as bucket storage options, which would let self-hosted setups run the PowerSync Service without a separate MongoDB or Postgres storage database. * New proposal: [Connection count & error metrics](https://github.com/orgs/powersync-ja/discussions/661). This would add a counter to our existing sync API metrics for the total number of completed sync connections, labelled by outcome, error code, close reason, and transport. You'd then be able to answer questions like "were there recent sync error spikes?" with a simple Prometheus query. ## Feature story: JavaScript SDKs v2 Over time, our JavaScript SDKs accumulated a lot of DX issues, and more recently AX issues too. So we took a step back and asked: what would make these SDKs better to work with and contribute to, for both humans and agents? We kept looking at the public API itself. `@powersync/common` exposed the SDK's internals alongside its public classes, so routine changes on our side could break existing builds or produce confusing type errors. Taking `AbstractPowerSyncDatabase` as an example, which was both a class and an effective public interface: even adding a method to it counted as a breaking change under TypeScript's structural rules. The day-to-day ergonomics had rough edges too. The options you passed to `PowerSyncDatabase` were spread across a nested chain of interfaces, so there was no single place to see what was available. The `database` field accepted a `DBAdapter`, an `SQLOpenFactory`, or open options all at once, so autocomplete showed every `DBAdapter` method instead of the options you wanted. The `js-logger` dependency caused problems when bundling. And WebSocket support was bundled whether you used it or not, which was not cheap: it relied on RSocket, whose packages are around 500 kB and need a custom build step to work outside Node.js. None of these were dealbreakers on their own, but together they made the SDKs harder to work with than they should be. And not just for people: coding agents tripped over the same confusing types and scattered options. So we fixed the foundation. Benefits of the JavaScript SDKs v2.0 release include: * A cleaner, more stable API, with only public interfaces exposed and implementation details in a separate package * Simpler setup, with one clearly-typed set of options for the database, and one for `connect()` * Logging as a small `PowerSyncLogger` interface rather than a separate dependency * Smaller bundles, with WebSockets loaded lazily and the two web workers merged into one * OP-SQLite built into `@powersync/react-native` as the default driver, replacing React Native Quick SQLite There are breaking changes, but for most apps the upgrade is a few mechanical edits to how you construct the database, connect, and set up logging. The [release notes](https://releases.powersync.com/announcements/v2-0-of-powersync-javascript-sdks) walk through every one. A lot of what went into v2.0 came straight out of your feedback and sharing how you're using these SDKs. Please keep it coming. That's it for this issue. --- # Building Our Company Brain: Agents + SQLite, Offline-Capable > How we built an internal "company brain" by aggregating data from 14+ systems into Postgres, syncing it to a local SQLite database with PowerSync, and giving agents a skill to query it — usable from Slack, a CLI, and even offline with local models. - Published: 2026-07-24 - Author: Conrad Hofmeyr - Category: Showcase - Source: https://powersync.com/blog/building-our-company-brain-agents-sqlite-offline-capable --- By now you've probably heard of the idea of a "second brain" or "company brain". Here's [YCombinator's Summer 2026 Request for Startups](https://www.youtube.com/shorts/IaWIazkWWog): > Every company has critical know-how scattered everywhere. Some of it lives in people's heads. Some of it is buried in old email accounts, Slack threads, support tickets, and databases. The company works because humans vaguely remember where that knowledge is and how to apply it. But AI agents can't operate like that. If we want every company to run on AI automation, we need a new primitive: a company brain. [...] A system that pulls knowledge out of all these fragmented sources, structures it, keeps it current, and turns it into an executable skills file for AI. In April 2026, Andrej Karpathy's tweet about research-focused [LLM Knowledge Bases](https://x.com/karpathy/status/2039805659525644595?lang=en) inspired a lot of people to simply have their agent use a folder of Markdown files to create and maintain a kind of 'second brain' with a collection of useful knowledge. YCombinator CEO Garry Tan created his [GBrain](https://github.com/garrytan/gbrain) project directly inspired by Karpathy's idea, for example. While a "folder of Markdown files" is elegant in its simplicity, there are many other ways to create a kind of 'second brain' using some combination of existing tools and technologies. There's now also dozens of companies aiming to solve this need, from startups building dedicated new AI brains and context layers, all the way to established database, ETL and data warehouse companies jumping on the bandwagon. ## Landing on SQLite For any team that's amassed even a moderate amount of historic knowledge, a functional 'company brain' is clearly very valuable. That motivated us to create our own around April. We really liked the simplicity of combining "a folder of markdown files" with the agents of our choice. We had some specific requirements in mind, however, that took us in a somewhat unique direction: 1. We wanted to use structured data where possible (not just unstructured text) with proper querying ability. 2. We wanted the 'brain' to be collaborative: multiple people in the team should be able to access and manage the same knowledge. 3. We wanted something self-hosted rather than putting our sensitive data into a 3rd party SaaS product. Ideally we wanted to use open-source tools. We have been a very pro-SQLite company since our founding, and so in light of our requirements, SQLite seemed like a natural choice as the foundation of the project. We know that agents grep'ing text files works superbly well, so our main question early on was: how well would it work if we *just gave an agent a SQLite database file*? In this post I wanted to share the architecture and results. And let me just preface that by saying: This is a specific opinionated flavor of a company brain that works for us, as a team of about ~30 people. Your mileage may vary! ## TL;DR: What We Built We built a set of simple Node.js-based ingesters that mostly use APIs & Webhooks to aggregate data from 14 (and counting) different systems like Google Drive, GitHub, Zendesk, Gong, HubSpot, Slack, etc. into a single Postgres database. The shape of the data is optimized for agent usability, using Markdown for free text, and rolling up data into columns in a single row containing JSON or Markdown where it makes sense. Then we used PowerSync to sync Postgres with a SQLite database, in which we create a full-text search (FTS) index. We dynamically generate a skill that gives any agent useful information about the schema and SQLite query conventions (including using the `sqlite3` CLI which ships with macOS and is readily available on Linux and Windows). Then we created two tools for the team that can be used based on preference: 1. A Slack bot that has a real-time synced SQLite database (using the PowerSync Node.js SDK), and uses Claude in headless mode with our provided skill to answer prompts when tagged in public Slack channels[^1]; and 2. A CLI tool that provides a synced SQLite database and our skill, so that you can use the company brain on your own machine with the agent of your choice. Since it's just SQLite, you can use it offline with local models. ## TL;DR: The Results This relatively simple architecture resulted in an incredibly useful tool. It turns out that the latest models and harnesses are really good at figuring out how to use a SQLite database. This includes running queries (including aggregations) over embedded JSON in the database – SQLite has really good built-in support for JSON. ![Screenshot of the company brain Slack bot answering a question about PostgreSQL failover support, citing a Zendesk ticket and an internal engineering document.](https://powersync.com/images/blog/building-our-company-brain-agents-sqlite-offline-capable-inline-slack-bot-answer.png) Our team quickly adopted the tool and it has saved us a huge amount of time on doing any kind of work that benefits from stitching together context across siloed systems. Our experience was very similar to what the YCombinator team [spoke about](https://www.youtube.com/watch?v=B246K_G7mHU&t=455s) when they built something similar with Postgres: > It didn't just make it easier to answer questions, it dramatically increased the number of questions that we would ask and dramatically increased the scale and complexity of the questions that we would dare to ask. If you're interested in a deeper dive of how we put it together, read on. ## Data Ingesters Here's a few examples of our ingesters to give you an idea of how we structure the data: * **Slack**: We have one Slack bot that doubles as both a data ingester and a prompt-answerer. It uses Slack's [Socket Mode](https://docs.slack.dev/apis/events-api/using-socket-mode/) to ingest data in real-time in any channels it's added to, and to listen for prompts. In Postgres, we store one row per channel per day. Whenever there's an event in that channel, we update the existing roll-up for that day. The whole channel conversation is rendered in Markdown format, including the contents of threads. We convert attachments like PDFs and images to Markdown using an LLM API. * **Google Drive:** For simplicity, we currently poll the Drive Changes API. We have a table with a column for the Markdown representation of each Drive file. Google Docs are easy to export as Markdown. We use the Sheets API to build nice Markdown tables of spreadsheets, which frontier LLMs do an excellent job of interpreting. PDFs get converted to Markdown too using an LLM API. For DOC/DOCX files, we use [Mammoth](https://www.npmjs.com/package/mammoth) to convert to HTML, and then convert that to Markdown. We also render all the comments on Drive files as Markdown, including their highlighted text. * **HubSpot:** We ingest all the *Companies* in our HubSpot system (which represent our customers), and embed the contacts for each company as a JSON column on the company row. The HubSpot company IDs are an identifier shared across several of our systems, which makes it easier for agents to associate data across different systems. * **Gong:** We ingest the transcripts of calls, and also use an LLM API to generate meeting notes in Markdown format. * **Stripe:** We ingest customers from Stripe, and build JSON embedded representations of each customer's subscriptions and invoicing history. These are simply stored as columns on the customer rows. * **GitHub:** We ingest Issues, PRs and Discussions – including their descriptions and comments. * **Product Analytics:** We already had a homebrewed product analytics system that stores its data in a Postgres database. When ingesting that data, we transform it into a single row per customer organization, containing a few JSON columns that aggregate useful high-level analytics. * **Gmail:** We ingest customer email threads, by comparing the domain names of the email participants with HubSpot Customers. Email conversations are rendered as Gmail-style threads, with quoted text and non-customer-facing messages stripped. PDF and image attachments are converted to Markdown using an LLM API. There is a "meta" table in Postgres which contains descriptive information about the schema. Each ingester's `schema.sql` definition file is responsible for doing an upsert into that table with its metadata. These help the agent with interpreting the database and knowing where to find what information (more on that below). ![Screenshot of the Postgres "meta" table with columns for table name, source system, description, and column comments.](https://powersync.com/images/blog/building-our-company-brain-agents-sqlite-offline-capable-inline-meta-table.png "The 'meta' table describes the schema so agents know where to find what information.") ## Syncing to SQLite For syncing from Postgres to SQLite, we define auto-subscribing [Sync Streams](https://docs.powersync.com/sync/streams/overview#sync-streams) in PowerSync for the relevant tables. We currently have a single company-wide dataset, but we could also use partial syncing for gating access to specific data. This would also allow scaling to a much larger total dataset – with users only syncing the part of the data that is relevant to them. ```yaml stripe_customers: auto_subscribe: true queries: - SELECT id, stripe_id, email, name, description, currency, balance, delinquent, created_at, subscriptions, invoices FROM stripe_customers WHERE archived = false ``` At our scale we currently have on the order of ~100k records being synced to SQLite, which takes about ~20 seconds for the initial sync, and then PowerSync keeps the SQLite database updated incrementally with deltas after that. ## Skill We dynamically generate a skill (based on a template) containing information to help the agent with using the SQLite database: * A schema description using the contents of the "meta" table mentioned above. * Usage and input/output guidelines for the `sqlite3` CLI * Guidelines on SQLite types, working with JSON, and using full-text search (FTS) * Suggested workflow for answering a question * Basic heuristics to decide what information is authoritative * Matching related data about an organization or person across tables (prefer shared identifiers where available, or use company domain name or email address, or fall back to proper noun matching) * Confidentiality note ("Do not transmit data outside the user's local environment without explicit permission") It might actually be sufficient to have a static skill with instructions on retrieving the schema description from the "meta" table, to avoid the step of dynamically generating the skill. We haven't tested that yet. ## Slack Bot The prompt-answering/Q&A part of the Slack bot is pretty straightforward. It listens for mentions of its name on any channels that it's a member of, and then launches `claude` in headless mode to execute prompts using the skill and SQLite database (which is synced in real-time using the PowerSync Node.js client SDK). If a schema change is detected, it currently resyncs the SQLite database (since this is a reasonably inexpensive operation). For security reasons, we block the Slack bot from answering questions in any Slack Connect channel, or any channel with outside guests. We also limit the tool calls that Claude can execute (`sqlite3`), disable web search/fetch, provide minimal allowlisted env vars, and limit the outbound network access of the machine where the bot runs. ## CLI Tool When the CLI tool is first launched, it opens an internal web app where the user has to sign in with their Google Workspace account (our main identity provider). That passes a longer-lived JWT back to the CLI. The CLI uses that longer-lived JWT with the internal web app's API to securely mint short-lived JWTs (valid for only 5 minutes) for PowerSync's authentication which allows syncing the SQLite database. If the longer-lived JWT expires or its associated session is revoked, the CLI wipes the SQLite database for security. Whenever the CLI is running, it keeps the SQLite database in sync in near real-time (we will probably turn this into a background daemon in the future). Any decent agent can use the SQLite database and skill provided by the CLI. ## It Works Offline, Running Locally On-Device Since we are giving the agent all the context it needs in a SQLite file, we can use the company brain offline with local models. We have used it with Ollama paired with either [Pi](https://pi.dev/) or [Claude Code](https://docs.ollama.com/integrations/claude-code). With models like [Gemma4 26B](https://deepmind.google/models/gemma/gemma-4/) that we've been using so far, the quality of company brain answers are obviously not on par with frontier models like Opus, Fable and GPT5, but are still respectable and provide a solid fallback if we happen to not have Internet access at any point[^2]. When the user goes back online, PowerSync will sync the deltas to bring the SQLite database up to date. This project serves as a reference implementation for how PowerSync can be used to provide a locally-available synced context layer (in SQLite) for "edge AI" / on-device AI use cases with local open-weight models. This was not specifically one of the original requirements for our 'company brain', but it turned out to be a useful benefit of the PowerSync-SQLite architecture. ## Roadmap This is phase 1. There's a lot of things we want to explore further. The first one will likely be bi-directional syncing. We can allow agents to codify knowledge in the database (e.g. customer intel, market knowledge, etc.). PowerSync allows us to control how SQLite mutations are processed into Postgres with server-authoritative logic in our backend application. Another one is keeping fresh copies of all our product Git repos as references that can be consulted when answering questions and performing work. This would give us even richer context and more precision. Agents will be able to combine data from all our systems with the product source code as ground truth. We have several other ideas that are percolating. ## Want to Use This? If you would like to run your own version of this and would like to use our implementation as a baseline, we will open-source it. [Let me know](mailto:conrad@powersync.com) if you're interested. ## Postgres Alternative? Instead of syncing the data from Postgres to SQLite, another possibility we superficially investigated is to define a skill that instructs the agent to query Postgres directly, using [`psql`](https://www.postgresql.org/docs/current/app-psql.html). We briefly tested this but didn't explore it deeply. With the PowerSync-SQLite approach, access control can be achieved by partially syncing the Postgres data to users based on their permissions, whereas for Postgres, [Row-Level Security (RLS)](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) may be suitable for access control. When using Postgres, the 'company brain' would also not be usable offline, and as mentioned above, an on-device AI reference implementation was one of the benefits we achieved with this project. However, using Postgres directly may be suitable for the needs of other companies. [^1]: The experience is similar to [Claude Tag](https://www.anthropic.com/news/introducing-claude-tag), which was launched by Anthropic a few weeks after we launched our internal company brain Slack bot. [^2]: Another idea we want to investigate is using something like [LiteLLM AI Gateway](https://docs.litellm.ai/docs/simple_proxy) to route prompts to cloud or local models based on their complexity. --- # Convex + PowerSync: Design Notes from the Experimental Release > Adding experimental Convex support: the design decisions we made, the parts of Convex that fit well, the rough edges, and the open questions we're still working through. - Published: 2026-06-17 - Author: Kobie Botha - Category: Engineering - Source: https://powersync.com/blog/convex-powersync-design-notes --- We've added experimental [Convex](https://www.convex.dev/) support to PowerSync, after a steady amount of requests for it from the community. This post is the engineering side of that work: the design decisions we made, the parts of Convex that fit PowerSync well, the rough edges, and the questions we're still working through. ## PowerSync in thirty seconds If you haven't used PowerSync before, here's briefly how it works. PowerSync keeps a backend database in sync with a local SQLite database embedded in your app, so the app reads and writes locally, is highly responsive, and keeps working whether or not it's online. There are two main pieces: the PowerSync Service, which runs on the server and connects to your source database, and a client SDK, which manages the local SQLite database on each device. On the read path, the Service sends each client only the rows it should see, based on *Sync Streams* you define. That's partial sync: every device holds its own subset of the data, not a copy of the whole database. On the write path, your app writes to local SQLite, the SDK queues the change, and that queue uploads through a path you control. For Convex, that path is your existing Convex mutations. ## Convex support is just another module PowerSync is designed to be stack-agnostic and each supported source database (currently: Postgres, MongoDB, MySQL, SQL Server, and now Convex) is its own [module](https://github.com/powersync-ja/powersync-service/tree/main/modules) on top of a shared core. The core handles what's common to every backend: the sync protocol, bucket storage (how replicated data is partitioned for efficient syncing), and checkpointing (tracking how far replication has progressed). Each module handles the database-specific replication. So adding Convex support came down to writing the Convex-specific replication code, and that's mostly what the rest of this post covers. ## How the Convex module replicates data PowerSync reads from Convex through its Streaming Export API, using three endpoints: `json_schemas` lists the tables, `list_snapshot` reads a full copy of a table, and `document_deltas` returns changes to data in the order they happened. When PowerSync first connects, it pins a single snapshot timestamp and reads every selected table at that timestamp. Because the timestamp stays fixed, the copy is consistent even though `list_snapshot` reads the tables one at a time. That initial copy is resumable. PowerSync records how far it got in each table, so a restart continues instead of starting over. This matters for large datasets, where the first copy can take a while. Once the copy is done, PowerSync switches to streaming. It polls `document_deltas` for new changes, starting from the same timestamp the snapshot was taken at, so no rows are missed and nothing is copied twice. For each change, it uses your Sync Streams to decide which rows to keep, how to transform them, and which buckets they belong to. Buckets are the partitions PowerSync stores replicated data in, so that later, when a client connects, the Service can hand it just the buckets it needs. ## What fit well A few things about Convex lined up well with how PowerSync already works. A Convex mutation is atomic, and every write in it shares one commit timestamp (`_ts`). Convex never splits those writes across two `document_deltas` pages, so we receive them together and commit them as a single transaction. A client never sees half a mutation. `document_deltas` also returns the complete document after each change, not just the fields that changed. That means we don't have to keep our own copy of the previous row to work out a diff, so we store less per row than we do for some other databases. Another big win was having a stable ID. Convex gives every document an `_id` that never changes. Our Postgres, MySQL and SQL Server replicators all have to track replica identity, because the columns that identify a row are configured per table and a row's identity can change underneath you. With Convex, `_id` is always the identity, so we skipped that whole class of bookkeeping. ## The checkpoint table PowerSync needs to know when a client's queued writes have been acknowledged by Convex. We get that signal from the Convex replication cursor: once replication reaches the cursor recorded for a write checkpoint, we can tell the client its write is durable. The catch is that the `document_deltas` cursor only advances when something writes to Convex. On an idle deployment, polling `document_deltas` returns the same cursor over and over, so a write checkpoint can sit in storage, correct but never delivered, because no later change ever moves replication up to it. Our fix is to write to Convex ourselves. After we record a write checkpoint, we call a `createCheckpoint` mutation that upserts a row in a small `powersync_checkpoints` table. That write produces a new entry in `document_deltas`, which advances the cursor. When the replicator sees the marker row it ignores it as user data, but the new cursor position is enough to release the write checkpoint to the client. You deploy the mutation; we call it. We're not doing anything unusual here, though on other backends this step is generally plumbing that the developer never has to touch. For example, in our Postgres replicator, we emit a logical replication message, which you never see, and with MongoDB we manage a `_powersync_checkpoints` collection for you. Ideally we'd like to remove this step. It would take a way to emit an ordered event into `document_deltas` without running a mutation, or a change to how we publish write checkpoints so we don't need the cursor to advance past an already-committed position. We wrote up our full analysis in [convex-write-checkpoints.md](https://github.com/powersync-ja/powersync-service/blob/main/docs/convex/convex-write-checkpoints.md). ## ID mapping with client-generated UUIDs Convex generates document IDs server-side, so a client can't know a row's `_id` until after it's inserted. PowerSync needs the opposite: a stable local ID the moment a row is created, since the row lives in local SQLite and the app reads and writes it before the upload reaches Convex. To bridge that, each replicated document carries a client-generated UUID, synced back to the client as `uuid AS id`. Convex keeps its own `_id`, the client uses the UUID, and your mutations map between the two on upload. This is the most visible DX cost of the integration today since every mutation has to handle the mapping. We looked into whether Convex could take a client-supplied ID instead, but from our analysis it would need significant changes on the Convex side. For now, it seems the UUID pattern is here to stay. ## Schema changes PowerSync reads each row's value straight from the JSON document in `list_snapshot` and `document_deltas`, not from Convex's `json_schemas` metadata. That's deliberate: `json_schemas` can omit a field until a document populates it, so reading types from it would tie a column's representation to whether Convex had reported that field yet. A nice side effect is that schema changes mostly handle themselves. Adding a field, removing one, or changing a type flows through `document_deltas` as a normal document change, with no re-snapshot. The exception is dropped tables. Deleting a table emits no per-document deletes, so rows already on clients can linger. The workaround is to clear the table first, or delete its documents through a mutation. We've documented this limitation and plan to handle it properly in a later release. ## Sync Streams instead of Convex server functions The Convex team has been candid that they're not fully comfortable with this part of the design, and we think it's worth discussing openly. In a Convex app, reads are TypeScript query functions deployed to Convex. Authorization lives in those functions, and Convex's reactivity pushes updates to subscribed clients. With PowerSync, the read path for synced data moves out of those functions. Which rows sync to each client is defined in [Sync Streams](https://docs.powersync.com/sync/streams/overview) (SQL-like queries the Service evaluates), and client reads run as SQL against local SQLite. Authorization you have in query functions gets expressed again in Sync Streams for the synced data. Sync Streams exist because of partial sync. A query function answers a query over the full database; a Sync Stream defines which subset reaches each device. Something has to declare that subset for offline-first, and server functions don't have a primitive for it today. Writes are also affected. Because the client works against SQLite, values that Convex stores as structured data sync down as their SQLite equivalents. A Convex object field, for example, arrives as JSON text, so a mutation that writes it back has to convert it. The demo app has a [basic example](https://github.com/powersync-community/powersync-convex-todolist-demo/blob/main/src/library/powersync/ConvexDecoder.ts) of this. So the tradeoff here is that you get SQL on the client (including joins), offline reads, and devices that only hold data they're allowed to see. You pay by reworking the read path of an existing app, the biggest piece of migration work, plus some conversion work on writes. New apps have less of this. ## Convex Components: the bigger open question Components are Convex's mechanism for packaging reusable backend modules into a Convex app. What if the entire PowerSync service ran as a Convex component? This is not impossible, but it would be a lot of work for us to implement, and we don't yet know whether the consistency guarantees PowerSync relies on would hold up. Still, if the integration gets traction, we're open to exploring it further. ## Try it out We shipped this early version to find out whether the current approach is good enough and to learn where the biggest gaps are. We want your feedback: it will directly influence whether – and how – this integration evolves. Get started with our [Setup Guide](https://docs.powersync.com/intro/setup-guide). Share your feedback by opening an issue on [GitHub](https://github.com/powersync-ja/powersync-service/issues) or join us in [Discord](https://discord.gg/powersync). --- # PowerSync Changelog: May 2026 > A wave of quality community PRs, open engineering discussions, Sync Streams reaching GA, and foundational work on the Dart and Swift SDKs. - Published: 2026-06-09 - Author: Kobie Botha - Category: Product Update - Source: https://powersync.com/blog/powersync-changelog-may-2026 --- Community PRs have been strong lately, and we're doubling down on open-source transparency by moving more engineering discussions into `#engineering-discussions` on our [Discord](https://discord.com/powersync) server. Thanks to all the contributors! On the product side, highlights were that Sync Streams reached GA, the Dart SDK got a major overhaul, the Swift SDK was rewritten in pure Swift, and you can now search and filter instance logs in the Dashboard. Grab another coffee, here's everything that happened over the past couple of months. ## Product updates shipped **PowerSync Service:** * **Sync Streams are now GA**: Since beta we added global CTEs, type-safe generated client wrappers, better error reporting with source offsets, and a long list of bug fixes. If you're still on Sync Rules, now's the time to migrate. Use the button in the dashboard or run `powersync migrate sync-rules` in the CLI. [Release notes](https://releases.powersync.com/announcements/sync-streams-are-now-generally-available) and [Docs](https://docs.powersync.com/sync/streams/overview). * **Earlier detection of Postgres WAL slot problems**: For Postgres source databases, if a replication slot is lost during initial replication, the Service now catches it early and stops rather than running to completion and failing. More importantly, WAL budget warnings now surface in the Dashboard and diagnostics API before a slot is lost, so you can fix the underlying conditions before they become a problem. [Docs](https://docs.powersync.com/maintenance-ops/replication-lag#postgres). * **HA replication for Pro plan+**: PowerSync Cloud instances on the Pro plan and above now get high-availability replication processes. **Client SDKs:** * **Dart/Flutter SDK v2.0 — consolidated and faster**: `powersync` is now the only package you need. We removed `powersync_core`, `powersync_sqlcipher`, and `powersync_flutter_libs`. Encryption is built in, the native connection pool is Rust-backed for faster queries and multi-isolate support, and web databases now default to OPFS on Chrome and Firefox. Learn more in our feature story below. * **Inspect your database in DevTools (Dart/Flutter)**: v2.1.0 adds a PowerSync tab to Dart & Flutter DevTools. Inspect open `PowerSyncDatabase` instances, view active Sync Streams and sync status, and run queries against the local SQLite database while debugging. [Docs](https://docs.powersync.com/tools/dart-devtools-extension). * **Swift SDK rewritten in pure Swift (v1.14.0)**: Previous versions wrapped a Kotlin XCFramework. The SDK is now native Swift throughout: no Kotlin dependency, smaller binary, better debuggability in Xcode, and typed CRUD payload fields. There are no breaking changes to the public API, but parts of the SDK are a full rewrite so we recommend smoke-testing your queries after upgrading. [Release notes](https://releases.powersync.com/announcements/swift-sdk-1-14-0-now-pure-swift). * **Concurrent reads and faster web writes (Web SDK v1.38.0)**: Added support for `OPFSWriteAheadVFS`, a new synchronous VFS using write-ahead logging. Reads no longer block on writes and multiple read queries run in parallel. Currently Chromium-only (it relies on OPFS's readwrite-unsafe mode). [Docs](https://docs.powersync.com/client-sdks/reference/javascript-web#2-opfs-based-alternatives). * **Attachments and Mac Catalyst for .NET (v0.1.2)**: The .NET SDK gained attachment sync APIs matching the implementation in other SDKs, plus Mac Catalyst support. [Changelog](https://releases.powersync.com/announcements/powersync-net-sdk). * **Improved raw tables across all SDKs**: `put` and `delete` statements are now inferred automatically. You only need to provide the `tableName`. Local-only columns and other table options now work with raw tables too. [Docs](https://docs.powersync.com/client-sdks/advanced/raw-tables). * **Capacitor SDK reached beta**: Production-ready for tested use cases and covered by our SLAs. New since alpha: Swift Package Manager support for iOS (requires Capacitor 8+), and significantly faster sync on native platforms via NDJSON-HTTP. [Docs](https://docs.powersync.com/client-sdks/reference/capacitor). **Tools:** * **Search and filter instance logs**: The Logs view in the PowerSync Dashboard now supports free-text search and structured `alias:value` filters (by `user_id`, `client_id`, error code, and more), plus CSV export. [Release notes](https://releases.powersync.com/announcements/searchable-instance-logs-in-the-powersync-dashboard) and [Docs](https://docs.powersync.com/maintenance-ops/monitoring-and-alerting#instance-logs). * **Self-service Private Endpoint setup**: Private Endpoints (AWS PrivateLink) can now be created and managed directly in the PowerSync Dashboard — no support ticket needed. Available on Team and Enterprise plans for Postgres and MongoDB Atlas. [Release notes](https://releases.powersync.com/announcements/self-service-private-endpoints-in-the-dashboard) and [Docs](https://docs.powersync.com/installation/database-setup/private-endpoints). * **Trigger compaction from the CLI**: `powersync compact` triggers compaction on your linked PowerSync Cloud instance directly, with an optional `--timeout` flag for large datasets. [Docs](https://docs.powersync.com/tools/cli). **New demos & guides:** * [Self-hosting on AWS EKS](https://docs.powersync.com/maintenance-ops/self-hosting/aws-eks) — step-by-step guide for deploying PowerSync on Kubernetes via Helm Charts. * [Dart/Flutter DevTools extension guide](https://docs.powersync.com/tools/dart-devtools-extension) — how to use the new database inspector for Dart/Flutter apps. * [Replication lag guide](https://docs.powersync.com/maintenance-ops/replication-lag) — what this metric means, common causes, and remediation steps. * [Diagnosing sync latency](https://docs.powersync.com/debugging/troubleshooting#diagnosing-sync-latency) — how to isolate which stage of the sync process is slow, and how to correlate user reports with sync session logs. ## Community feed * We added a [PowerSync add-on to the TanStack CLI](https://github.com/TanStack/cli/pull/407). Run `create powersync-app --framework react --add-ons powersync` to get a working PowerSync setup for React. * Steven built a [MikroORM + PowerSync PoC](https://github.com/powersync-community/mikro-orm-poc). [Here's the discussion](https://github.com/powersync-ja/powersync-js/discussions/896) if you have thoughts or want to help take it further. * Dev wrote a series on the role sync engines can play in AI apps: [most AI chat apps throw away in-flight responses on refresh or disconnect — treating LLM output as synced state fixes that](https://powersync.com/blog/most-ai-chat-apps-are-broken-sync-engines-are-the-fix), [why AI apps default to single-player and how sync engines bring multiplayer collaboration](https://powersync.com/blog/why-is-every-ai-app-single-player), and a [two-part practical guide](https://powersync.com/blog/building-ai-powered-apps-part-1) on building collaborative, synced AI apps ([Part 2](https://powersync.com/blog/building-ai-powered-apps-part-2)). * Simon wrote up [Easy Tauri Integration Tests with Vitest](https://powersync.com/blog/tauri-integration-tests) — useful if you're building with the Tauri SDK. * New engineering proposals: * **Write API** — Christiaan is exploring a first-class write protocol for PowerSync, covering mutators, conflict handling, and how writes flow from client to backend. This would be a significant change to how you build with PowerSync and we want input from people actually building with it. Join the [discussion in Discord](https://discord.com/channels/1138230179878154300/1504213613009834145/1504851899806126201). * **Raw Table High Performance Diffs** — trigger-based diffing specifically optimized for raw tables, where the current approach has limitations. [Proposal](https://docs.google.com/document/d/12bvZDJF2aaTOqkxHyo3W-TYGQTaS9FQFdu-MX2yOuqI/edit). * **Faster and incremental `sync_local`** — a more internal proposal, with the goal to make initial sync faster and eventually incremental, so it can report progress and avoid blocking writes for minutes on large datasets. [Discussion #178](https://github.com/powersync-ja/powersync-sqlite-core/discussions/178). * **Streaming attachments** — the attachment queue currently loads the entire file into JS memory, which can be a problem with large files. [Discussion #968](https://github.com/powersync-ja/powersync-js/discussions/968). * [@austinbhale](https://github.com/austinbhale): thank you for building out attachments support for the .NET SDK, bringing it to parity with our other SDKs! [PR #68](https://github.com/powersync-ja/powersync-dotnet/pull/68). * Thank you also to [@Shamyyoun](https://github.com/powersync-ja/powersync-kotlin/pull/352), [@sravan27](https://github.com/powersync-ja/powersync-service/pull/644), [@VIVAAN-DHAWAN](https://github.com/powersync-ja/powersync-js/pull/970), [@Sagbyy](https://github.com/powersync-ja/powersync-kotlin/pull/348), [@johnnysedh3lllo](https://github.com/powersync-ja/powersync-js/pull/925), and [@JexanJoel](https://github.com/powersync-ja/powersync-js/pull/924) for your contributions. * Built with PowerSync: * _Fig_ by the team at [figwealth.io](https://figwealth.io): freelancer finance — expense tracking, invoicing, P&L, and tax estimates. [App Store](https://apps.apple.com/us/app/fig-finance-for-freelancers/id6478242586) and [Google Play](https://play.google.com/store/apps/details?id=io.figwealth.app). * _Thunderbolt_ by the team behind Mozilla: an open-source, self-hostable enterprise AI client with chat, search, and research workflows across devices. [Blog post](https://www.thunderbolt.io/blog/mozilla-introduces-thunderbolt). * _Capubridge_ by [@aybinv7](https://github.com/aybinv7): a Tauri desktop devtool for debugging WebView-based Android apps, with direct SQLite inspection. Useful for anyone debugging PowerSync sync state on Capacitor or React Native. [GitHub](https://github.com/aybinv7/capubridge). ## Feature story: The Dart SDK gets a proper foundation The Dart/Flutter SDK has in many ways been our flagship SDK. It was the first SDK we shipped, and it's still one of the most widely used. But it had accumulated a fair amount of technical debt: separate packages for core functionality, encryption, and Flutter libs that you had to wire up yourself; a legacy Dart sync client running in parallel with the Rust one; and some platform-specific rough edges we've been working around. On the web in particular, the setup was annoying: manual WASM file copying, and Safari requiring special cross-origin isolation headers just to use storage. v2.0 addresses that. `powersync` is now the only package you need, whether you're building a Flutter app, a CLI tool, or a server-side Dart service. SQLite loading is now automatic through build hooks — you no longer need `powersync_flutter_libs` or a manual initialization step. Encryption is built in. On native, the connection pool is now Rust-backed, which makes queries faster and lets you safely open the same database across isolates or Flutter engines. On the web, new databases now default to the more performant OPFS on Chrome and Firefox. The sync and database workers are also merged into one file now, so there's less to configure and your compiled output is smaller. We're now doing the same kind of work on the JavaScript SDK. A few open proposals and PRs give a sense of where it's headed: [merging the web workers](https://github.com/powersync-ja/powersync-js/discussions/949) into one (same as we did in Dart), [making WebSocket support an optional dependency](https://github.com/powersync-ja/powersync-js/discussions/950) so you don't bundle it if you don't need it, and [dropping js-logger](https://github.com/powersync-ja/powersync-js/pull/966) as a dependency in favor of letting you bring your own. A good open-source SDK should be easy to use and easy to contribute to. We want both, and that's what inspired this. That's it for this issue. --- # The Journey of Rebuilding Our Dashboard > How we rebuilt the PowerSync Dashboard by dogfooding our own sync engine in production. - Published: 2026-06-04 - Author: Manrich van Greunen - Category: Engineering - Source: https://powersync.com/blog/rebuilding-powersync-dashboard --- When we set out to rebuild the PowerSync Dashboard, we decided it would be more than a technical migration and UI refresh. It was a chance to dogfood our own product in production and share what we learned along the way. This post documents that journey. # Why We Rebuilt: Learning from User Feedback ## Three Goals Driving the Rebuild The most common feedback from users was that the original dashboard felt “confusing and unintuitive” compared to modern tools like Supabase. The original dashboard was built using existing JourneyApps infrastructure and a framework designed for a different use case. The experience was fragmented: features like user account management and billing were relegated to a separate portal, forcing users to jump between tabs just to manage a single organization. Driven by this feedback, we set out with three clear objectives for a new architecture. First, we wanted to replace the IDE-like complexity and problematic UX with something simpler. Second, we committed to dogfooding our own sync engine to power the dashboard’s data layer. Finally, we wanted this project to serve as a best practices reference for our community, demonstrating how to build a multi-tenant SaaS application with PowerSync on the web. ## Backend & API Architecture ### Our Existing Backend The PowerSync Cloud control-plane is composed of **CRUD/RPC-style APIs** that are backed by our existing **MongoDB** databases. ### First Iteration of the New Dashboard The new dashboard was built using a conventional Single Page Application (SPA) stack. This included **React 19** as the core library, **Vite** for efficient bundling, and **Turborepo** for managing the monorepo structure. For client-side routing, we first chose **React Router 7**, and the user interface was built with **shadcn/ui** components. Data fetching and mutation handling against the backend APIs were managed using **TanStack Query**, with **Axios** serving as the primary HTTP client. ## PowerSync Dogfooding Using PowerSync for the dashboard required us to engage with the same documentation, use the same APIs, and confront the same decisions that our users face. A critical decision to make when building with sync engines is defining sync boundaries. For the dashboard, we chose a **hybrid architecture** that plays to the strengths of both sync and traditional APIs. ### Decision: Read-Path Only Sync The dashboard manages a sophisticated hierarchy of entities: **Users, Organizations, Plans, Projects, Instances, Operations, Logs, and Metrics**. Our backend provides APIs for mutations on these entities, but the read-path was where we saw the most potential for improvement with a sync engine. The dashboard **read path** includes data that is read frequently but changed relatively rarely. Because the dashboard is responsible for managing critical infrastructure, we require server-side validation and auditing. While the PowerSync **write path** offers optimistic updates, the resulting complexity of handling asynchronous rollbacks and server-side validation can be significant. Therefore, we decided to initially go with the simpler API-first **write path** that provides synchronous server-side validation and auditing. ### What We Sync (And What We Don’t) The current iteration syncs **Operations** and **Instance Configurations** to SQLite, which include **Sync Config** definitions. Entities like **User Profiles** and **Plans**, which are currently distributed across our microservices, remain on standard API calls, with a plan to unify and integrate them into the sync layer in the future. By contrast, we do not sync high-volume or temporal data like **Logs** and **Metrics**. Sensitive authentication flows and **Billing and** **Usage** operations also remain behind standard API calls. This hybrid approach makes navigating to an instance feel instantaneous because the data is already in the local SQLite database. Loading spinners have effectively vanished from the main navigation flow, and the UI updates in real-time. ### From Sync Rules to Sync Streams: A Real-World Evolution The most significant learning came from actually using PowerSync at scale for our own dashboard. Here's how our approach evolved: #### Sync Rules with JWT Parameters Our first iteration utilized the classic [**Sync Rules**](https://docs.powersync.com/sync/rules/overview) system, the method for defining what data is synchronized to the client. We configured it to embed the required `project_id` in the JWT as shown in the configuration below: ```sql bucket_definitions: project_data: parameters: select request.jwt() ->> 'project_id' as project_id data: - SELECT * FROM instances WHERE project_id = bucket.project_id - SELECT * FROM operations WHERE project_id = bucket.project_id ``` This worked, but critically, switching between projects required generating a new JWT, disconnecting and reconnecting to the PowerSync Service, and waiting for the full sync to complete. These delays and loading states were breaking the instant and fluid UI that sync engines are meant to deliver. #### Next Iteration: Sync Streams We migrated to our new [**Sync Streams**](https://docs.powersync.com/sync/streams/overview) system, which provides native support for on-demand syncing. In the Sync Streams implementation, we pass an array of `project_ids`, and check it against the JWT for proper authorization. See [Sync Streams: Using Parameters](https://docs.powersync.com/sync/streams/parameters) for more detail. ```sql streams: instances: query: SELECT * FROM instances WHERE project_id = subscription.parameter('project_id') AND project_id IN auth.parameter('project_ids') operations: query: SELECT * FROM operations WHERE project_id = subscription.parameter('project_id') AND project_id IN auth.parameter('project_ids') # Note: at the time of writing, edition: 3 is available and recommended config: edition: 2 ``` Now, switching projects simply subscribes to a new stream. If a user switches back to a previous project, the data is already in SQLite and appears instantly #### The “Too many parameter query results” issue During migration, we hit an edge case where the service attempted to resolve our `IN` clauses as independent queries, leading to an explosion of intermediate results and a "Too many parameter query results" error. We fixed this by converting the `IN` clause to a static check: ```sql WHERE project_id = subscription.parameter('project_id') AND (subscription.parameter('project_id') IN auth.parameter('project_ids')) ``` This specific insight has directly informed our new [**Sync Streams Compiler**](https://github.com/powersync-ja/powersync-service/pull/451), which automatically optimizes these patterns for all users. ### Dogfooding Insights: Local-Only Tables While building the Sync Config page, we needed a way to store local drafts as users were editing their Sync Config. We realized that we can unify our data layer and simplify state management by using a **Local-Only** table. In this way we can use the same `useQuery` hooks for both synced cloud data and temporary local drafts. For reference here is the table definition: ```ts export const DraftSyncStreamsTable = { tableDefinition: sqliteTable('draft_sync_streams', { id: text('id').primaryKey(), instance_id: text('instance_id'), valid: integer({ mode: 'boolean' }), definition: text('definition'), created_at: integer('created_at', { mode: 'timestamp' }), updated_at: integer('updated_at', { mode: 'timestamp' }) }), options: { localOnly: true } }; ``` And the `useDraftSyncStreamQuery` hook: ```ts import { db } from '~/lib/powersync'; import { useQuery } from '@powersync/tanstack-react-query'; import { toCompilableQuery } from '@powersync/drizzle-driver'; export const useDraftSyncStreamQuery = ({ instanceId }: SyncStreamVariable) => { const query = toCompilableQuery( db.select().from(DraftSyncStreamsTable).where(eq(DraftSyncStreamsTable.id, instanceId)).limit(1) ); return useQuery({ queryKey: `${instanceId}-drafts`, query }); }; ``` ## Modern Web Stack: The TanStack Router Pivot Story We started with React Router 7, but a hackday exploration of [TanStack Router](https://tanstack.com/router/latest) changed our trajectory. After migrating just two routes, we decided to switch. We get file-based routing that makes sense and type safety that makes it nearly impossible to break a link. TanStack Router with its great TypeScript developer experience provides us 100% autocompletion for route paths and search parameters. It provides flexible route declarations of nested, layout or pathless routes. With automatic prefetching, it makes navigation feel local. We were really impressed and migrated the entire dashboard in a day. ## Development Velocity Through AI & Process ### v0 for Wireframes to Initial UI We used [v0](https://v0.app/) to bridge the gap between wireframes and code. By feeding wireframes into v0, we generated shadcn-based components that got us most of the way to the finish line, allowing us to focus our energy on logic rather than CSS. ### Context Engineering with `AGENTS.md` AI assistance is only as good as the context you provide. Maintaining a strict `AGENTS.md` file not only ensures that every AI output adheres to our technical standards (like strict TypeScript types and accessibility patterns) but also significantly increases development velocity by ensuring code consistency and reducing review time. Here is a snippet of our AI guidelines: ``` # AI Assistant Guidelines ## Core Development Philosophy - **KISS**: Keep It Simple, Stupid - **YAGNI**: You Aren't Gonna Need It ## TypeScript Configuration (STRICT REQUIREMENTS) - **NEVER use `any` type** - use `unknown` if type is truly unknown - **MUST have explicit return types** for all functions - **MUST use `ReactElement` instead of `JSX.Element`** for React 19 ## Component Guidelines - **MAXIMUM 200 lines** per component file - **MUST handle ALL states**: loading, error, empty, and success - **MUST verify actual prop names** before using components ## TanStack Query Patterns - Query keys: Hierarchical factory pattern - Mutations: Automatic cache invalidation - Prefetch hooks for performance-critical paths ## Form Components - **MUST use shadcn/ui Field components** for accessibility - **Required Pattern**: `Field` + `FieldLabel` + `FieldDescription` + `FieldError` - **Validation**: Zod schemas with `.catch()` for graceful failures ``` ## Deployment: Managing Environment Variables Shipping a modern SPA to multiple environments using a single Docker image presented a classic challenge: how to inject environment variables without rebuilding the image. Standard Vite builds bake variables into the bundle at build time, but we wanted a more flexible approach. We used [import-meta-env](https://import-meta-env.org/) to inject these variables at runtime via a simple bash script. We used Node.js's **Single Executable Application** feature to bundle the injection script into a single binary, keeping our deployment pipeline fast and dependency-free. ## Conclusion Rebuilding the PowerSync Dashboard was as much a product exercise as a frontend project, since it was a great chance to dogfood our own sync engine. We are currently working toward the next phase, which includes open-sourcing the dashboard and preparing a version for self-hosted infrastructure. If you have questions about our stack or our architecture, find us on [Discord](https://discord.gg/powersync). --- # Easy Tauri Integration Tests with Vitest > Having a hard time writing integration tests for Tauri? It's very easy with this little trick, even on macOS! - Published: 2026-05-14 - Author: Simon Binder - Category: Engineering - Source: https://powersync.com/blog/tauri-integration-tests --- Recently, we introduced our [PowerSync SDK for Tauri](https://releases.powersync.com/announcements/introducing-the-powersync-tauri-sdk-alpha). The SDK is a Tauri plugin, meaning that it consists of two parts: 1. A Rust crate used to access a local SQLite database, sync changes via PowerSync, and notify JavaScript apps about changes. 2. A small JavaScript library acting as a type-safe wrapper around the raw Tauri IPC commands supported by our Rust crate. As part of the development work, we also looked at ways to test our new SDK. Tauri basically offers [three modes of testing](https://v2.tauri.app/develop/tests/): 1. A [`MockRuntime`](https://docs.rs/tauri/latest/tauri/test/struct.MockRuntime.html) for Rust, allowing us to write unit tests for our Rust crate by mocking out JavaScript. 2. A [fake Tauri environment](https://v2.tauri.app/develop/tests/mocking/) for JavaScript, allowing us to write unit tests for our JavaScript package by mocking out Rust. 3. A [WebDriver](https://v2.tauri.app/develop/tests/webdriver/) integration, allowing us to test everything together. Especially when writing Tauri plugins, options 1 and 2 provide very little value. How our JavaScript and Rust sources interact is the most interesting thing we want to test, as our SDK wouldn't work if anything about that was wrong. Two tests each mocking the other half provide no guarantees. So the only option left available to us was to spin up a demo app and use WebDriver support built into Tauri for integration tests. However, that option has its own big issues: 1. It's designed to test Tauri apps, while we're interested in testing plugin functionality. 2. It doesn't work on macOS, which is what most of us use. In the end, we found a neat way to very reliably test everything in our Tauri plugin nonetheless: Instead of driving an app through WebDriver, what if we launched a Tauri app that simply... tested itself? After all, this is exactly how we test most of our JavaScript nowadays: Instead of mocking web or React APIs to run tests in Node, we let vitest spawn a browser and use the real thing. That loads a web page running our tests, reporting results back to a local vitest server which will print them and set an appropriate exit code for CI. ## Using Tauri as a vitest browser Starting from version 4, vitest has pretty decent support for custom browsers. So our plan to test our SDK was fairly straightforward: 1. Write a tiny Tauri app that allows loading any URL and loads the PowerSync Tauri plugin. 2. Tell vitest to launch that app with a custom URL instead of spawning a browser. You can see the whole thing [in action here](https://github.com/powersync-ja/powersync-js/tree/main/packages/tauri). The `main.rs` for our test app is very simple: It receives a URL as a CLI argument before opening a window with that URL. Using `dev_url` skips some IPC checks and simplifies the setup: ```rust use std::env; use url::Url; fn main() { // Use default options, but open window with URL from args. let mut context = tauri::generate_context!(); if let Some(url) = env::args().skip(1).next() { let config = context.config_mut(); config.build.dev_url = Some(Url::parse(&url).expect("Could not parse URL")); } tauri::Builder::default() .plugin(tauri_plugin_powersync::init()) .run(context) .expect("error while running tauri application"); } ``` For permissions in `capabilities/default.json`, we used these: ```json { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "enables the default permissions", "windows": ["*"], "remote": { "urls": ["http://127.0.0.1:*"] }, "permissions": [ "core:default", "powersync:default", "core:webview:allow-create-webview-window", "core:window:allow-set-title" ] } ``` This would not be a good idea for real Tauri apps, but permission checks would just stand in the way for integration tests. To launch this app in vitest, we wrote a custom `BrowserProvider` in `vitest.config.ts`: ```ts const serverFactory = preview().serverFactory; // Relative path to the integration test runner app built with cargo. const testRunnerExecutable = path.resolve('../../target/debug/test-runner'); class TauriBrowserProvider implements BrowserProvider { #tauriApp?: ChildProcess; #isClosing = false; // ... some boring methods omitted async openPage(_sessionId: string, url: string, _options: { parallel: boolean; }) { if (this.#tauriApp != null) { throw new Error('TODO: Calling openPage multiple times is not supported'); } // Ensure the target app spawning webviews is up-to-date. const buildResult = spawnSync('cargo', ['build', '-p', 'test-runner'], { stdio: 'inherit' }); if (buildResult.status !== 0) { throw new Error(`cargo build failed with exit code ${buildResult.status}`); } const app = spawn(testRunnerExecutable, [url]); this.#tauriApp = app; app.on('exit', (code) => { if (!this.#isClosing) { console.log('Test runner exited with code', code); process.exit(1); } }); await new Promise((resolve, reject) => { app.once('spawn', () => resolve()); app.once('error', reject); }); } async close() { this.#isClosing = true; this.#tauriApp?.kill(); } } ``` And that's it! In our `defineConfig` block, we can then use this provider as a custom browser: ```ts export default defineConfig({ test: { include: ['tests/**/*.test.ts'], isolate: false, browser: { enabled: true, provider: { name: 'tauri-app', options: {}, providerFactory() { return new TauriBrowserProvider(); }, serverFactory }, instances: [ // We just need any bogus instance here { browser: 'chrome' } ] }, } }); ``` This is enough to run tests, which now run within a real Tauri app: ![A tauri app running the vitest browser UI](https://powersync.com/images/blog/tauri-test-runner.png) Actually using Tauri APIs requires a small workaround: Tauri installs global definitions into the window which is then used as an IPC hook. Since vitest uses an `