Skip to main content
Used in conjunction with Serverpod, PowerSync enables developers to build local-first apps that are robust in poor network conditions and that have highly responsive frontends while relying on Serverpod for shared models in a full-stack Dart project. This guide walks you through configuring PowerSync within your Serverpod project.

Overview

PowerSync works by:
  1. Automatically streaming changes from your Postgres backend source database into a SQLite database on the client.
  2. Collecting local writes that users have performed on the SQLite database, and allowing you to upload those writes to your backend.
See Architecture Overview for a full overview.
To integrate PowerSync into a Serverpod project, a few aspects need to be considered:

Database setup

Your Serverpod models need to be persisted into a Postgres database.

PowerSync configuration

PowerSync needs access to your Postgres database to stream changes to users.

Authentication

To ensure each user only has access to the data they’re supposed to see, Serverpod authenticates users against PowerSync.

Data sync

After configuring your clients, your Serverpod projects are offline-ready!
This guide shows all steps in detail. Here, we assume you’re working with a fresh Serverpod project. You can follow along by creating a notes project using the Serverpod CLI:
Of course, all steps and migrations also apply to established projects.

Database Setup

Begin by configuring your Postgres database for PowerSync. PowerSync requires logical replication to be enabled. With the docker-compose.yaml file generated by Serverpod, add a command to the postgres service to enable this option. This is also a good opportunity to add a health check, which helps PowerSync connect at the right time later:
You can also find sources for the completed demo in this repository. More information about setting up Postgres for PowerSync is available here.
Next, configure existing models to be persisted in the database. In the template created by Serverpod, edit notes_server/lib/src/greeting.spy.yaml:
PowerSync works best when ids are stable. And since clients can also create rows locally, using randomized ids reduces the chance of collisions. This is why we prefer UUIDs over the default incrementing key.
After making the changes, run serverpod generate and ignore the issues in greeting_endpoint.dart for now. Instead, run serverpod create-migration and note the generated path:
We will use the migration adding the greeting table to also configure a replication that PowerSync will hook into. For that, edit notes_server/migrations/<migration id>/migration.sql At the end of that file, after COMMIT;, add this:
To restrict read access to specific tables, explicitly list allowed tables for both the SELECT privilege, and for the publication mentioned in the next step (as well as for any other publications that may exist). This is also a good place to set up a Postgres publication that a PowerSync Service will subscribe to:
Note that the PowerSync Service has to read all updates present in the publication, regardless of whether the table is referenced in your Sync Streams / Sync Rules definitions. This can cause large spikes in memory usage or introduce replication delays, so if you’re dealing with large data volumes, you’ll want to specify a comma-separated subset of tables to replicate instead of FOR ALL TABLES.
The snippet above replicates all tables and is the simplest way to get started in a dev environment.
After adding these statements to migration.sql, also add them to definition.sql. The reason is that Serverpod runs that file when instantiating the database from scratch, migration.sql would be ignored in that case.

PowerSync Configuration

PowerSync requires a service to process Postgres writes into a form that can be synced to clients. Additionally, your Serverpod backend will be responsible for generating JWTs to authenticate clients as they connect to this service. To set that up, begin by generating an RSA key to sign these JWTs. In the server project, run dart pub add jose to add a package supporting JWTs in Dart. Then, create a tool/generate_keys.dart that prints a new key pair when run: Run dart run tool/generate_jwt.dart and save its output, it’s needed for the next step as well. For development, you can add the PowerSync Service to the compose file. It needs access to the source database, a Postgres database to store intermediate data, and the public half of the generated signing key.
To configure PowerSync, create a file called service.yaml next to the compose file. This file configures how PowerSync connects to the source database, how to authenticate users, and which data to sync:
More information on available options is available under Service Configuration

Authentication

PowerSync processes the entire source database into buckets, an efficient representation for sync. With the configuration shown here, there is one such bucket per user storing all greetings owned by that user. For security, it is crucial each user only has access to their own bucket. This is why PowerSync gives you full access control:
  1. When a client connects to PowerSync, it fetches an authentication token from your Serverpod instance.
  2. Your Dart backend logic returns a JWT describing what data the user should have access to.
  3. In the sync_rules section, you reference properties of the created JWTs to control data visible to the connecting clients.
In this guide, we will use a single virtual user for everything. For real projects, follow Serverpod documentation on authentication. PowerSync needs two endpoints, one to request a JWT and one to upload local writes from clients to the backend source database. In notes_server/lib/src/powersync_endpoint.dart, create those endpoints:
You can delete the existing greeting_endpoint.dart file, it’s not necessary since PowerSync is used to fetch data from your server. Also remove invocations related to future calls in lib/server.dart. Don’t forget to run serverpod generate afterwards.

Data Sync

With all services, configured, it’s time to spin up development services:
With your Serverpod backend and PowerSync running, you can start connecting your clients. Go to the _flutter project generated by Serverpod and run dart pub add powersync path path_provider. Next, replace main.dart with this demo:
Ensure containers are running (docker compose up), start your backend dart run bin/main.dart in notes_server and finally launch your app. When the app is loaded, you should see a greeting synced from the server. To verify PowerSync is working, here are some things to try:
  1. Update in the source database: Connect to the Postgres database again (psql -h 127.0.0.1 -p 8090 -U postgres) and run a query like update greeting set message = upper(message);. Note how the app’s UI reflects these changes without you having to write any code for these updates.
  2. Click on a delete icon to see local writes automatically being uploaded to the backend.
  3. Add new items to the database and stop your backend to simulate being offline. Deleting items still updates the client immediately, changes will be written to Postgres as your backend comes back online.

Next Steps

This guide demonstrated a minimal setup with PowerSync and Serverpod. To expand on this, you could explore:
  • Web support: PowerSync supports Flutter web, but needs additional assets.
  • Authentication: If you already have an existing backend that is publicly-reachable, serving a JWKS URL would be safer than using pre-shared keys.
  • Deploying: The easiest way to run PowerSync is to let us host it for you (you still have full control over your source database and backend). You can also explore self-hosting the PowerSync Service.