Menu
Blog Documentation Community Pricing Demo Call Sign Up
Sign Up

PowerSync: Offline-First Sync for Postgres

Learn how PowerSync keeps client-side SQLite in sync with Postgres — sync rules, offline writes, client SDKs, and deployment options.

General

This post was written by an engineer at QueryPlane. QueryPlane is an app builder for your database: bring your own postgres db and you can create interactive applications to share with other developers, coworkers or even your customers. If you’re interested in trying it out, get started here.


Applications that depend on constant network connectivity break in predictable ways. A field worker inspecting equipment in a basement with no signal, a retail POS system during an internet outage, a delivery driver entering data in a rural dead zone—all of these are scenarios where the traditional request-response model falls apart. PowerSync is a sync engine that addresses this by keeping a local SQLite database on the client in sync with a backend Postgres (or MongoDB, or MySQL) database, enabling full offline functionality with automatic background synchronization.

In this post, we’ll cover:

  • How PowerSync works - Bidirectional sync between server databases and client-side SQLite
  • Sync Rules and buckets - The partitioning model that controls what data reaches each client
  • The write path - How offline writes queue and upload through your backend API
  • Client SDKs - Flutter, React Native, web frameworks, and more
  • Deployment options - Managed cloud vs. self-hosted
  • Tradeoffs - Where PowerSync fits and where it gets complicated

How PowerSync works

PowerSync sits between your backend database and your client applications. On the server side, it connects to PostgreSQL using logical replication to capture every insert, update, and delete from the WAL—the same change data capture mechanism used by tools like Debezium. For MongoDB, it uses change streams; for MySQL, binlogs.

On the client side, PowerSync manages an embedded SQLite database. Your application reads from and writes to this local database using standard SQL queries. The sync engine handles replicating server-side changes down to the client and uploading client-side mutations back to the server, all in the background.

The result is that your application code interacts with a local SQLite database that happens to stay in sync with the server. Reads are instant because they hit local storage. Writes are instant because they’re applied locally first and queued for upload. If the network disappears, the app keeps working against the local database and catches up when connectivity returns.

Sync Rules and the bucket model

Not every client needs every row from every table. PowerSync uses Sync Rules to define which data subsets reach which clients. Sync Rules are written in a YAML-based DSL and evaluated server-side to determine data partitioning.

bucket_definitions:
  user_tasks:
    parameters: SELECT request.user_id() as user_id
    data:
      - SELECT * FROM tasks WHERE user_id = bucket.user_id
      - SELECT * FROM projects WHERE id IN (SELECT project_id FROM tasks WHERE user_id = bucket.user_id)

Under the hood, Sync Rules produce buckets—partitions of data scoped to specific users, organizations, or any other dimension you define. A bucket ID combines the definition name with parameter values (e.g., user_tasks["42"]). When multiple users share the same scope—say, members of the same organization—they share a single bucket, avoiding redundant data storage on the server.

Each bucket maintains a history of operations, not just the latest state. This enables efficient incremental sync: when a client reconnects after being offline, it only needs to fetch the operations it missed since its last sync checkpoint, rather than re-downloading everything. Automatic compaction prevents this history from growing unbounded.

The bucket model also provides a natural security boundary. Since Sync Rules are evaluated server-side, clients never see data outside their assigned buckets. This is fundamentally different from approaches where filtering happens on the client—with PowerSync, unauthorized data never leaves the server.

The write path: upload queue

PowerSync’s write path is designed around developer control. When your application performs an insert, update, or delete on the local SQLite database, the change is applied locally and simultaneously added to a persistent FIFO upload queue. The queue survives app restarts and network failures.

You define an uploadData() function in your client code that determines how queued mutations are sent to your backend:

class MyConnector extends PowerSyncBackendConnector {
  async uploadData(database: AbstractPowerSyncDatabase) {
    const batch = await database.getCrudBatch();
    if (!batch) return;

    for (const op of batch.crud) {
      switch (op.op) {
        case "PUT":
          await api.post(`/api/${op.table}`, op.opData);
          break;
        case "PATCH":
          await api.patch(`/api/${op.table}/${op.id}`, op.opData);
          break;
        case "DELETE":
          await api.delete(`/api/${op.table}/${op.id}`);
          break;
      }
    }
    await batch.complete();
  }
}

This design means your backend retains full control over write validation, authorization, and business logic. The upload function calls your existing API endpoints—the same ones your web app might already use. PowerSync doesn’t bypass your backend or write directly to the database on the client’s behalf.

The tradeoff is that conflict resolution is your responsibility. If two offline clients edit the same row, both mutations will eventually reach your backend through the upload queue. Your API needs to handle this—whether through last-write-wins, application-level merge logic, or rejecting conflicts. PowerSync provides the infrastructure for getting mutations to the server; how you resolve conflicts is up to you.

Client SDKs

PowerSync provides SDKs across a wide range of platforms:

PlatformSDK
Flutter/Dartpowersync-sdk-flutter
React Native/Expo@powersync/react-native
Web (React, Vue, Svelte)@powersync/web
Kotlin Multiplatformpowersync-kotlin
Swiftpowersync-swift

All SDKs share the same core pattern: you define a schema that maps to your synced tables, initialize a PowerSync database, and query it with SQL. The SDKs provide live queries (also called watch queries) that automatically re-execute when underlying data changes, making it straightforward to build reactive UIs.

const tasks = usePowerSyncWatchedQuery(
  "SELECT * FROM tasks WHERE project_id = ? ORDER BY created_at DESC",
  [projectId]
);

The web SDK uses WASM-compiled SQLite with OPFS for persistence, which means the local database survives page reloads. For more on how browser-based SQLite works, see our post on local-first databases.

See what QueryPlane can build for you

Connect to your database, write SQL with AI, and build shareable apps — all from your browser.

Deployment options

PowerSync Cloud is the managed service. You register your database connection, define Sync Rules in the dashboard, and point your client SDKs at the cloud endpoint. PowerSync handles replication, bucket management, scaling, and monitoring. The service achieved SOC 2 compliance and HIPAA compliance in January 2026.

Pricing starts with a free tier (2 GB synced data/month, 50 concurrent connections) and scales through Pro ($49/month), Team ($599/month), and Enterprise plans. Billing is based on data synced per month and concurrent connections rather than per-row charges.

Self-hosted deployment is available in two editions. The Open Edition is free and includes the core sync engine, Sync Rules, and all client SDKs. The Enterprise Self-Hosted edition adds email SLA support, SOC 2 reports, and an assigned engineer.

For either deployment model, your Postgres database needs logical replication enabled (wal_level = logical). Most managed PostgreSQL providers support this, though some require enabling it through a configuration panel.

Supported backend databases

While PowerSync started as a Postgres sync engine, it now supports multiple backend databases:

PostgreSQL is the primary and most mature integration, using logical replication for change capture. MongoDB reached GA in March 2025 with change stream-based replication. MySQL support is in beta, using binlog-based replication. SQL Server is in alpha, using CDC (Change Data Capture).

The client-side database is always SQLite, regardless of which backend you use. This means your client-side queries are SQL even if your backend is MongoDB—PowerSync handles the translation between the server’s data model and the client’s relational schema.

Tradeoffs

PowerSync’s bidirectional sync model is its main advantage over read-only sync engines like ElectricSQL. Applications that need genuine offline write capability—field service tools, mobile data collection, retail POS systems—benefit directly from the persistent upload queue and local-first write path.

The flip side is complexity. You need to implement the uploadData() function, handle conflict resolution in your backend, and think carefully about what happens when two clients edit the same data while offline. For applications that don’t need offline writes, this is unnecessary overhead.

Sync Rules provide powerful data partitioning, but they’re another piece of configuration to manage and reason about. As your data model grows and the relationships between tables become more complex, Sync Rules can become harder to maintain. Testing that your Sync Rules produce the correct buckets for every user scenario requires deliberate effort.

The local SQLite database also has practical limits. If a user’s bucket contains millions of rows, initial sync can be slow and the local database may consume significant storage on the device. PowerSync is best suited for applications where each client needs a manageable subset of the total data—thousands to tens of thousands of rows, not millions.

Wrapping up

PowerSync fills a specific gap in the sync engine landscape: bidirectional, offline-first sync between server databases and client-side SQLite, with developer control over the write path. The bucket-based partitioning model provides both efficient sync and a natural security boundary, and the upload queue ensures offline mutations aren’t lost.

If your application needs to work reliably without a network connection and you’re running Postgres on the backend, PowerSync is one of the more production-ready options available, with customers like Halliburton and Mi9 Retail running it at scale. For a broader view of the sync engine landscape, see our comparison of ElectricSQL, PowerSync, and Replicache. For more on the local-first movement and the technologies underpinning it, check out our post on local-first databases.