From 356d950f67f9db9d78a8194248f77f89f94fb6a5 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 20 Oct 2025 15:20:13 +0200 Subject: [PATCH 01/12] Notes on using Node SDK with encryption --- client-sdk-references/node.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/client-sdk-references/node.mdx b/client-sdk-references/node.mdx index 5c5234f8..cac0af5c 100644 --- a/client-sdk-references/node.mdx +++ b/client-sdk-references/node.mdx @@ -210,8 +210,6 @@ See [Supported Platforms -> Node.js SDK](/resources/supported-platforms#node-js- The SDK has an optional dependency on `better-sqlite3` which is used as the default SQLite driver for that package. -Because that dependency is optional, it can be replaced or removed to customize how SQLite -gets loaded. This section lists common options. ### Encryption From ab68d6ed50b923cd8152d7e3cef10f5665f567f1 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 20 Oct 2025 15:22:04 +0200 Subject: [PATCH 02/12] Complete paragraph --- client-sdk-references/node.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client-sdk-references/node.mdx b/client-sdk-references/node.mdx index cac0af5c..5c5234f8 100644 --- a/client-sdk-references/node.mdx +++ b/client-sdk-references/node.mdx @@ -210,6 +210,8 @@ See [Supported Platforms -> Node.js SDK](/resources/supported-platforms#node-js- The SDK has an optional dependency on `better-sqlite3` which is used as the default SQLite driver for that package. +Because that dependency is optional, it can be replaced or removed to customize how SQLite +gets loaded. This section lists common options. ### Encryption From 1b79f0519d1db7536898cb2ed10b9a7dc0ab5a8b Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 31 Oct 2025 21:29:52 +0100 Subject: [PATCH 03/12] Initial draft for Serverpod guide --- docs.json | 1 + integration-guides/serverpod.mdx | 366 +++++++++++++++++++++++++++++++ 2 files changed, 367 insertions(+) create mode 100644 integration-guides/serverpod.mdx diff --git a/docs.json b/docs.json index c9765c00..6b8aa870 100644 --- a/docs.json +++ b/docs.json @@ -232,6 +232,7 @@ "integration-guides/flutterflow-+-powersync/powersync-+-flutterflow-legacy" ] }, + "integration-guides/serverpod", "integration-guides/railway-+-powersync", "integration-guides/coolify" ] diff --git a/integration-guides/serverpod.mdx b/integration-guides/serverpod.mdx new file mode 100644 index 00000000..4c7123b4 --- /dev/null +++ b/integration-guides/serverpod.mdx @@ -0,0 +1,366 @@ +--- +title: "ServerPod + PowerSync" +description: "Easily make your ServerPod projects offline-ready with PowerSync" +sidebarTitle: "Serverpod" +--- + +import PostgresPowerSyncUser from '/snippets/postgres-powersync-user.mdx'; +import PostgresPowerSyncPublication from '/snippets/postgres-powersync-publication.mdx'; + +Used in conjunction with [ServerPod](https://serverpod.dev/), 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 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 Postgres. + +See [Architecture Overview](/architecture/architecture-overview) for a full overview. + +To integrate PowerSync into a Serverpod project, a few aspects need to be considered: + + + + Your Serverpod models need to be persisted into a Postgres database. + + + PowerSync needs access to your Postgres database to stream changes to users. + + + To ensure each user only has access to the data they're supposed to see, Serverpod + authenticates users against PowerSync. + + + 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 prooject: + +``` +serverpod create notes +``` + +Of course, all steps and migrations also apply to established projects. + +## Database setup + +Begin by [setting up the source database](/installation/database-setup). 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: + +```yaml +services: + # Development services + postgres: + image: pgvector/pgvector:pg16 + ports: + - "8090:5432" + command: ["postgres", "-c", "wal_level=logical"] # Added for PowerSync + environment: + POSTGRES_USER: postgres + POSTGRES_DB: notes + # ... + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] + interval: 5s + timeout: 5s + retries: 5 + volumes: + - notes_data:/var/lib/postgresql/data + +``` + +Next, configure existing models to be persisted in the database. In the template created by +Serverpod, edit `notes_server/lib/src/greeting.spy.yaml`: + +```yaml +### A greeting message which can be sent to or from the server. +class: Greeting + +table: greeting # Added table key + +fields: + ### Important! Each model used with PowerSync needs to have a UUID id column. + id: UuidValue,defaultModel=random,defaultPersist=random + ### The user id owning this greeting, used for access control in PowerSync + owner: String + + ### The greeting message. + message: String + ### The author of the greeting message. + author: String + ### The time when the message was created. + timestamp: DateTime +``` + +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: + +``` +$ serverpod create-migration + +✓ Creating migration (87ms) + • Migration created: migrations/ +✅ Done. +``` + +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.sql` +At the end of that file, after `COMMIT;`, add this: + + + +This is also a good place to setup a Postgres publication that a PowerSync service will subscribe to: + + + +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: + + +```dart +import 'dart:convert'; +import 'dart:math'; + +import 'package:jose/jose.dart'; + +void main() { + var generatedKey = JsonWebKey.generate('RS256').toJson(); + final kid = 'powersync-${generateRandomString(8)}'; + generatedKey = {...generatedKey, 'kid': kid}; + + print(''' + JS_JWK_N: ${generatedKey['n']} + PS_JWK_E: ${generatedKey['e']} + PS_JWK_KID: $kid +'''); + + final encodedKeys = base64Encode(utf8.encode(json.encode(generatedKey))); + print('JWT signing keys for backend: $encodedKeys'); +} + +String generateRandomString(int length) { + final random = Random.secure(); + final buffer = StringBuffer(); + + for (var i = 0; i < length; i++) { + const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + + buffer.writeCharCode(alphabet.codeUnitAt(random.nextInt(alphabet.length))); + } + + return buffer.toString(); +} +``` + + +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. + +```yaml +services: + powersync: + restart: unless-stopped + image: journeyapps/powersync-service:latest + depends_on: + postgres: + condition: service_healthy + command: ["start", "-r", "unified"] + volumes: + - ./powersync.yaml:/config/config.yaml + environment: + POWERSYNC_CONFIG_PATH: /config/config.yaml + # Use the credentials created in the previous step, the /notes is the datase name for Postgres + PS_SOURCE_URI: "postgresql://powersync_role:myhighlyrandompassword@postgres:5432/notes" + PS_STORAGE_URI: "postgresql://powersync_role:myhighlyrandompassword@postgres:5432/powersync_storage" + JS_JWK_N: # output from generate_keys.dart + PS_JWK_E: AQAB # output from generate_keys.dart + PS_JWK_KID: # output from generate_keys.dart + ports: + - 8095:8080 +``` + +To configure PowerSync, create a file called `powersync.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: + +```yaml +replication: + connections: + - type: postgresql + uri: !env PS_SOURCE_URI + + # SSL settings + sslmode: disable # 'verify-full' (default) or 'verify-ca' or 'disable' + +# Connection settings for sync bucket storage +storage: + type: postgresql + uri: !env PS_STORAGE_URI + sslmode: disable # 'verify-full' (default) or 'verify-ca' or 'disable' + +# The port which the PowerSync API server will listen on +port: 8080 + +sync_rules: + content: | + streams: + todos: + # For each user, sync all greeting they own. + query: SELECT * FROM greeting WHERE owner = request.user_id() + auto_subscribe: true # Sync by default + config: + edition: 2 + +client_auth: + audience: [powersync] + jwks: + keys: + - kty: RSA + n: !env PS_JWK_N + e: !env PS_JWK_E + alg: RS256 + kid: !env PS_JWK_KID +``` + +More information on available options is available under [PowerSync Service Setup](/self-hosting/installation/powersync-service-setup) + +## Authentication + +PowerSync processes the entire source database [into buckets](/usage/sync-rules/organize-data-into-buckets), an efficient representation +for sync. With the configuration shown here, there is one such bucket per user storing all `greeting`s 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 acecss 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](https://docs.serverpod.dev/tutorials/guides/authentication). + +PowerSync needs two endpoints, one to request a JWT and one to upload local writes from clients to the backend database. +In `notes_server/lib/src/powersync_endpoint.dart`, create those endpoints: + +```dart +import 'dart:convert'; +import 'dart:isolate'; + +import 'generated/protocol.dart'; +import 'package:serverpod/serverpod.dart'; +import 'package:jose/jose.dart'; + +class PowerSyncEndpoint extends Endpoint { + Future createJwt(Session session) async { + // TODO: Throw if the session is unauthenticated. + + // TODO: Extract user-id from session outsie + final userId = 'global_user'; + final token = await Isolate.run(() => _createPowerSyncToken(userId)); + + // Also create default greeting if none exist for this user. + if (await session.db.count(where: Greeting.t.author.equals(userId)) == 0) { + await Greeting.db.insertRow( + session, + Greeting( + owner: userId, + message: 'Hello from Serverpod and PowerSync', + author: 'admin', + timestamp: DateTime.now(), + ), + ); + } + + return token; + } + + /// Upload a batch of local writes to the backend database. + Future applyChanges(Session session, List greetings) async { + // TODO: Throw if the session is unauthenticated. + await session.db.transaction((tx) async { + for (final greeting in greetings) { + // TODO: Throw if the user is not allowed to write to this greeting. + final existing = + await Greeting.db.findById(session, greeting.id, transaction: tx); + if (existing == null) { + await Greeting.db.insertRow(session, greeting, transaction: tx); + } else { + await Greeting.db.updateRow(session, greeting, transaction: tx); + } + } + }); + } +} + +Future _createPowerSyncToken(String userId) async { + final decoded = _jsonUtf8.decode(base64.decode(_signingKey)); + final signingKey = JsonWebKey.fromJson(decoded as Map); + + final now = DateTime.now(); + + final builder = JsonWebSignatureBuilder() + ..jsonContent = { + 'sub': userId, + 'iat': now.millisecondsSinceEpoch ~/ 1000, + 'exp': now.add(Duration(minutes: 10)).millisecondsSinceEpoch ~/ 1000, + 'aud': ['powersync'], + 'kid': _keyId, + } + ..addRecipient(signingKey, algorithm: 'RS256'); + + final jwt = builder.build(); + return jwt.toCompactSerialization(); +} + +final _jsonUtf8 = JsonCodec().fuse(Utf8Codec()); + +const _signingKey = 'TODO'; // The "JWT signing keys for backend" bit from tool/generate_keys.dart +const _keyId = 'TODO'; // PS_JWK_KID from tool/generate_keys.dart +``` + +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: + +``` +docker compose down +docker compose up --detach --scale powersync=0 + +# This creates the PowerSync role +dart run bin/main.dart --role maintenance --apply-migrations + +# Create the PowerSync bucket storage database, use password from docker-compose.yaml +psql -h 127.0.0.1 -p 8090 -U postgres + +Password from user postgres: +postgres=# CREATE DATABASE powersync_storage WITH OWNER = powersync_role; +postgres=# \q + +docker compose down +``` + +## Next steps + +TODO: Guide to cloud version. From cd8fc8ea9a33067ced31c66fa699f5cb57061885 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 3 Nov 2025 16:20:55 +0100 Subject: [PATCH 04/12] Finish serverpod guide --- integration-guides/serverpod.mdx | 290 +++++++++++++++++++++++++++++-- 1 file changed, 272 insertions(+), 18 deletions(-) diff --git a/integration-guides/serverpod.mdx b/integration-guides/serverpod.mdx index 4c7123b4..19aca509 100644 --- a/integration-guides/serverpod.mdx +++ b/integration-guides/serverpod.mdx @@ -1,6 +1,6 @@ --- title: "ServerPod + PowerSync" -description: "Easily make your ServerPod projects offline-ready with PowerSync" +description: "Easily add offline-capable sync to your ServerPod projects with PowerSync" sidebarTitle: "Serverpod" --- @@ -16,7 +16,7 @@ This guide walks you through configuring PowerSync within your Serverpod project PowerSync works by: 1. Automatically streaming changes from your Postgres backend 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 Postgres. +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](/architecture/architecture-overview) for a full overview. @@ -42,6 +42,8 @@ This guide shows all steps in detail. Here, we assume you're working with a fres You can follow along by creating a prooject: ``` +# If you haven't already, dart pub global activate serverpod_cli + serverpod create notes ``` @@ -66,7 +68,7 @@ services: POSTGRES_USER: postgres POSTGRES_DB: notes # ... - healthcheck: + healthcheck: # Added for PowerSync test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] interval: 5s timeout: 5s @@ -99,6 +101,12 @@ fields: timestamp: DateTime ``` + +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: @@ -277,7 +285,7 @@ class PowerSyncEndpoint extends Endpoint { final token = await Isolate.run(() => _createPowerSyncToken(userId)); // Also create default greeting if none exist for this user. - if (await session.db.count(where: Greeting.t.author.equals(userId)) == 0) { + if (await Greeting.db.count(session) == 0) { await Greeting.db.insertRow( session, Greeting( @@ -292,22 +300,26 @@ class PowerSyncEndpoint extends Endpoint { return token; } - /// Upload a batch of local writes to the backend database. - Future applyChanges(Session session, List greetings) async { + Future createGreeting(Session session, Greeting greeting) async { // TODO: Throw if the session is unauthenticated. + await Greeting.db.insertRow(session, greeting); + } + + Future updateGreeting(Session session, UuidValue id, + {String? message}) async { + // TODO: Throw if the session is unauthenticated, or if the user should not + // be able to update this greeting. await session.db.transaction((tx) async { - for (final greeting in greetings) { - // TODO: Throw if the user is not allowed to write to this greeting. - final existing = - await Greeting.db.findById(session, greeting.id, transaction: tx); - if (existing == null) { - await Greeting.db.insertRow(session, greeting, transaction: tx); - } else { - await Greeting.db.updateRow(session, greeting, transaction: tx); - } - } + final row = await Greeting.db.findById(session, id); + await Greeting.db.updateRow(session, row!.copyWith(message: message)); }); } + + Future deleteGreeting(Session session, UuidValue id) async { + // TODO: Throw if the session is unauthenticated, or if the user should not + // be able to delete this greeting. + await Greeting.db.deleteWhere(session, where: (tbl) => tbl.id.equals(id)); + } } Future _createPowerSyncToken(String userId) async { @@ -358,9 +370,251 @@ Password from user postgres: postgres=# CREATE DATABASE powersync_storage WITH OWNER = powersync_role; postgres=# \q -docker compose down +# Start PowerSync service +docker compose up --detach + +# Start backend +dart run bin/main.dart +``` + +## Data sync + +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: + +```dart +import 'package:flutter/foundation.dart'; +import 'package:notes_client/notes_client.dart'; +import 'package:flutter/material.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:powersync/powersync.dart' hide Column; +import 'package:powersync/powersync.dart' as ps; +import 'package:serverpod_flutter/serverpod_flutter.dart'; + +/// Sets up a global client object that can be used to talk to the server from +/// anywhere in our app. The client is generated from your server code +/// and is set up to connect to a Serverpod running on a local server on +/// the default port. You will need to modify this to connect to staging or +/// production servers. +/// In a larger app, you may want to use the dependency injection of your choice +/// instead of using a global client object. This is just a simple example. +late final Client client; + +late final PowerSyncDatabase db; + +late String serverUrl; + +void main() async { + // When you are running the app on a physical device, you need to set the + // server URL to the IP address of your computer. You can find the IP + // address by running `ipconfig` on Windows or `ifconfig` on Mac/Linux. + // You can set the variable when running or building your app like this: + // E.g. `flutter run --dart-define=SERVER_URL=https://api.example.com/` + const serverUrlFromEnv = String.fromEnvironment('SERVER_URL'); + final serverUrl = + serverUrlFromEnv.isEmpty ? 'http://$localhost:8080/' : serverUrlFromEnv; + + client = Client(serverUrl) + ..connectivityMonitor = FlutterConnectivityMonitor(); + + db = PowerSyncDatabase( + // For more options on defining the schema, see https://docs.powersync.com/client-sdk-references/flutter#1-define-the-schema + schema: Schema([ + ps.Table('greeting', [ + ps.Column.text('owner'), + ps.Column.text('message'), + ps.Column.text('author'), + ps.Column.text('timestamp'), + ]) + ]), + path: await getDatabasePath(), + logger: attachedLogger, + ); + await db.initialize(); + await db.connect(connector: ServerpodConnector(client.powerSync)); + + Object? lastError; + db.statusStream.listen((status) { + final error = status.anyError; + if (error != null && error != lastError) { + debugPrint('PowerSync error: $error'); + } + + lastError = error; + }); + + runApp(const MyApp()); +} + +Future getDatabasePath() async { + const dbFilename = 'powersync-demo.db'; + // getApplicationSupportDirectory is not supported on Web + if (kIsWeb) { + return dbFilename; + } + final dir = await getApplicationSupportDirectory(); + return join(dir.path, dbFilename); +} + +final class ServerpodConnector extends PowerSyncBackendConnector { + final EndpointPowerSync _service; + + ServerpodConnector(this._service); + + @override + Future fetchCredentials() async { + final token = await _service.createJwt(); + return PowerSyncCredentials( + endpoint: 'http://localhost:8095', + token: token, + ); + } + + @override + Future uploadData(PowerSyncDatabase database) async { + if (await database.getCrudBatch() case final pendingWrites?) { + for (final write in pendingWrites.crud) { + if (write.table != 'greeting') { + throw 'TODO: handle other tables'; + } + + switch (write.op) { + case UpdateType.put: + await _service.createGreeting(Greeting.fromJson(write.opData!)); + case UpdateType.patch: + await _service.updateGreeting( + UuidValue.fromString(write.id), + message: write.opData!['message'] as String?, + ); + case UpdateType.delete: + await _service.deleteGreeting(UuidValue.fromString(write.id)); + } + } + + await pendingWrites.complete(); + } + } +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Serverpod Demo', + theme: ThemeData(primarySwatch: Colors.blue), + home: const GreetingListPage(), + ); + } +} + +final class GreetingListPage extends StatelessWidget { + const GreetingListPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('PowerSync + Serverpod'), + actions: const [_ConnectionState()], + ), + body: StreamBuilder( + stream: + db.watch('SELECT id, message, author FROM greeting ORDER BY id'), + builder: (context, snapshot) { + if (snapshot.hasData) { + return ListView( + children: [ + for (final row in snapshot.requireData) + _GreetingRow( + key: ValueKey(row['id']), + id: row['id'], + message: row['message'], + author: row['author'], + ), + ], + ); + } else if (snapshot.hasError) { + return Text(snapshot.error.toString()); + } else { + return const CircularProgressIndicator(); + } + }, + ), + ); + } +} + +final class _GreetingRow extends StatelessWidget { + final String id; + final String message; + final String author; + + const _GreetingRow( + {super.key, + required this.id, + required this.message, + required this.author}); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Row( + children: [ + Expanded(child: Text(message)), + IconButton( + onPressed: () async { + await db.execute('DELETE FROM greeting WHERE id = ?', [id]); + }, + icon: Icon(Icons.delete), + color: Colors.red, + ), + ], + ), + subtitle: Text('Greeting from $author'), + ); + } +} + +final class _ConnectionState extends StatelessWidget { + const _ConnectionState({super.key}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: db.statusStream, + initialData: db.currentStatus, + builder: (context, snapshot) { + final data = snapshot.requireData; + return Icon(data.connected ? Icons.wifi : Icons.cloud_off); + }, + ); + } +} ``` +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 -TODO: Guide to cloud version. +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](/client-sdk-references/flutter/flutter-web-support). +- Authentication: If you already have an existing backend that is publicly-reachable, serving a [JWKS URL](https://docs.powersync.com/installation/authentication-setup/custom) + would be safer than using pre-shared keys. +- Deploying: The easiest way to run PowerSync is to [let us host it for you](https://accounts.journeyapps.com/portal/powersync-signup) + (you still have full control over your source database and backend). + You can also explore [self-hosting](https://docs.powersync.com/intro/powersync-overview) the PowerSync service. From f7cac78c0e27b6acd2f133b63976a38f85fa7550 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 3 Nov 2025 16:28:50 +0100 Subject: [PATCH 05/12] Link to repo --- integration-guides/serverpod.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integration-guides/serverpod.mdx b/integration-guides/serverpod.mdx index 19aca509..6a7280c1 100644 --- a/integration-guides/serverpod.mdx +++ b/integration-guides/serverpod.mdx @@ -78,6 +78,8 @@ services: ``` +You can also find sources for the completed demo [in this repository](https://github.com/powersync-community/powersync-serverpod-demo). + Next, configure existing models to be persisted in the database. In the template created by Serverpod, edit `notes_server/lib/src/greeting.spy.yaml`: From 93dd620292a9859637c90b2bf9e7493c16fba2be Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 3 Nov 2025 16:47:49 +0100 Subject: [PATCH 06/12] Add existing Dart backend examples --- resources/demo-apps-example-projects.mdx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/resources/demo-apps-example-projects.mdx b/resources/demo-apps-example-projects.mdx index 98efc108..cda5f6f3 100644 --- a/resources/demo-apps-example-projects.mdx +++ b/resources/demo-apps-example-projects.mdx @@ -21,6 +21,11 @@ This page showcases example projects organized by platform and backend technolog * [Simple Chat App](https://github.com/powersync-ja/powersync.dart/tree/master/demos/supabase-simple-chat) * [Trello Clone App](https://github.com/powersync-ja/powersync-supabase-flutter-trello-demo) + #### Dart Custom Backend + + * [Built with Serverpod](https://github.com/powersync-community/powersync-serverpod-demo) + * [Built with `shelf`, `riverpod` and `drift`](https://github.com/powersync-community/self-host-dart-fullstack) + #### Node.js Custom Backend * [To-Do List App with Firebase Auth](https://github.com/powersync-ja/powersync.dart/tree/main/demos/firebase-nodejs-todolist) From 916ec6bcbbfb15e43ef457e3885ab7da6515d18d Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 3 Nov 2025 16:48:22 +0100 Subject: [PATCH 07/12] jaspr!! --- resources/demo-apps-example-projects.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/demo-apps-example-projects.mdx b/resources/demo-apps-example-projects.mdx index cda5f6f3..7f078d0c 100644 --- a/resources/demo-apps-example-projects.mdx +++ b/resources/demo-apps-example-projects.mdx @@ -23,8 +23,8 @@ This page showcases example projects organized by platform and backend technolog #### Dart Custom Backend - * [Built with Serverpod](https://github.com/powersync-community/powersync-serverpod-demo) - * [Built with `shelf`, `riverpod` and `drift`](https://github.com/powersync-community/self-host-dart-fullstack) + * [Built with Flutter and Serverpod](https://github.com/powersync-community/powersync-serverpod-demo) + * [Built with Jaspr, `shelf`, `riverpod` and `drift`](https://github.com/powersync-community/self-host-dart-fullstack) #### Node.js Custom Backend From e3b8408d20689d10f4dbb9d753c0e2a296b3483b Mon Sep 17 00:00:00 2001 From: Benita Volkmann Date: Tue, 4 Nov 2025 11:05:01 +0200 Subject: [PATCH 08/12] Rename sidebartitles and add servpod to Vale whitelist --- .github/vale/config/vocabularies/PowerSync/accept.txt | 1 + docs.json | 4 ++-- integration-guides/coolify.mdx | 2 +- integration-guides/railway-+-powersync.mdx | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/vale/config/vocabularies/PowerSync/accept.txt b/.github/vale/config/vocabularies/PowerSync/accept.txt index 351e3654..12b93185 100644 --- a/.github/vale/config/vocabularies/PowerSync/accept.txt +++ b/.github/vale/config/vocabularies/PowerSync/accept.txt @@ -116,6 +116,7 @@ Tauri shadcn Jepsen WorkManager +Serverpod SQLCipher SQLDelight Riverpod diff --git a/docs.json b/docs.json index 6b8aa870..7f87b3e2 100644 --- a/docs.json +++ b/docs.json @@ -213,7 +213,7 @@ "pages": [ "integration-guides/integrations-overview", { - "group": "Supabase + PowerSync", + "group": "Supabase", "pages": [ "integration-guides/supabase-+-powersync", "integration-guides/supabase-+-powersync/handling-attachments", @@ -223,7 +223,7 @@ ] }, { - "group": "FlutterFlow + PowerSync", + "group": "FlutterFlow", "pages": [ "integration-guides/flutterflow-+-powersync", "integration-guides/flutterflow-+-powersync/handling-attachments", diff --git a/integration-guides/coolify.mdx b/integration-guides/coolify.mdx index c069fcc7..7ec560d7 100644 --- a/integration-guides/coolify.mdx +++ b/integration-guides/coolify.mdx @@ -1,6 +1,6 @@ --- title: "Deploy PowerSync Service on Coolify" -sidebarTitle: "Coolify + PowerSync" +sidebarTitle: "Coolify" description: "Integration guide for deploying the [PowerSync Service](http://localhost:3333/architecture/powersync-service) on Coolify" --- diff --git a/integration-guides/railway-+-powersync.mdx b/integration-guides/railway-+-powersync.mdx index eaef1e90..8f6ca309 100644 --- a/integration-guides/railway-+-powersync.mdx +++ b/integration-guides/railway-+-powersync.mdx @@ -1,6 +1,7 @@ --- title: "Railway + PowerSync" description: "Integration guide for deploying a Postgres database and custom backend using Railway for Postgres and Node.js hosting." +sidebarTitle: "Railway" --- Railway is an attractive alternative to managed solutions such as Supabase, well suited to users looking for more control without going the full IaaS route. From f2195430525eed6afc1428161d06cee4b2deee5a Mon Sep 17 00:00:00 2001 From: Benita Volkmann Date: Tue, 4 Nov 2025 11:09:22 +0200 Subject: [PATCH 09/12] Serverpod casing --- integration-guides/serverpod.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration-guides/serverpod.mdx b/integration-guides/serverpod.mdx index 6a7280c1..81ecb860 100644 --- a/integration-guides/serverpod.mdx +++ b/integration-guides/serverpod.mdx @@ -1,13 +1,13 @@ --- -title: "ServerPod + PowerSync" -description: "Easily add offline-capable sync to your ServerPod projects with PowerSync" +title: "Serverpod + PowerSync" +description: "Easily add offline-capable sync to your Serverpod projects with PowerSync" sidebarTitle: "Serverpod" --- import PostgresPowerSyncUser from '/snippets/postgres-powersync-user.mdx'; import PostgresPowerSyncPublication from '/snippets/postgres-powersync-publication.mdx'; -Used in conjunction with [ServerPod](https://serverpod.dev/), PowerSync enables developers to build local-first apps that are robust in poor network conditions +Used in conjunction with [Serverpod](https://serverpod.dev/), 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. From b1d4a892f832f4d6431655bd33aab5daa6c93e2f Mon Sep 17 00:00:00 2001 From: Benita Volkmann Date: Tue, 4 Nov 2025 11:19:40 +0200 Subject: [PATCH 10/12] typo --- integration-guides/serverpod.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-guides/serverpod.mdx b/integration-guides/serverpod.mdx index 81ecb860..36435789 100644 --- a/integration-guides/serverpod.mdx +++ b/integration-guides/serverpod.mdx @@ -39,7 +39,7 @@ To integrate PowerSync into a Serverpod project, a few aspects need to be consid 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 prooject: +You can follow along by creating a `notes` project using the Serverpod CLI: ``` # If you haven't already, dart pub global activate serverpod_cli From 88a08f72140b6782929927046ddc0b0b2ad33464 Mon Sep 17 00:00:00 2001 From: Benita Volkmann Date: Tue, 4 Nov 2025 11:27:31 +0200 Subject: [PATCH 11/12] PowerSync Service uppercase and typo --- integration-guides/serverpod.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/integration-guides/serverpod.mdx b/integration-guides/serverpod.mdx index 36435789..0031b137 100644 --- a/integration-guides/serverpod.mdx +++ b/integration-guides/serverpod.mdx @@ -126,7 +126,7 @@ At the end of that file, after `COMMIT;`, add this: -This is also a good place to setup a Postgres publication that a PowerSync service will subscribe to: +This is also a good place to setup a Postgres publication that a PowerSync Service will subscribe to: @@ -182,7 +182,7 @@ String generateRandomString(int length) { 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. +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. @@ -261,7 +261,7 @@ for sync. With the configuration shown here, there is one such bucket per user s 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 acecss to. +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 @@ -372,7 +372,7 @@ Password from user postgres: postgres=# CREATE DATABASE powersync_storage WITH OWNER = powersync_role; postgres=# \q -# Start PowerSync service +# Start PowerSync Service docker compose up --detach # Start backend @@ -619,4 +619,4 @@ This guide demonstrated a minimal setup with PowerSync and Serverpod. To expand would be safer than using pre-shared keys. - Deploying: The easiest way to run PowerSync is to [let us host it for you](https://accounts.journeyapps.com/portal/powersync-signup) (you still have full control over your source database and backend). - You can also explore [self-hosting](https://docs.powersync.com/intro/powersync-overview) the PowerSync service. + You can also explore [self-hosting](https://docs.powersync.com/intro/powersync-overview) the PowerSync Service. From aa33becf3752f3fcc2f3eb0734594be1894ab650 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 4 Nov 2025 11:23:54 +0100 Subject: [PATCH 12/12] Review feedback --- integration-guides/serverpod.mdx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/integration-guides/serverpod.mdx b/integration-guides/serverpod.mdx index 0031b137..146d3611 100644 --- a/integration-guides/serverpod.mdx +++ b/integration-guides/serverpod.mdx @@ -51,7 +51,7 @@ Of course, all steps and migrations also apply to established projects. ## Database setup -Begin by [setting up the source database](/installation/database-setup). PowerSync requires logical replication +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: @@ -78,7 +78,10 @@ services: ``` -You can also find sources for the completed demo [in this repository](https://github.com/powersync-community/powersync-serverpod-demo). + +You can also find sources for the completed demo [in this repository](https://github.com/powersync-community/powersync-serverpod-demo). +More information about setting up Postgres for PowerSync is available [here](/installation/database-setup). + Next, configure existing models to be persisted in the database. In the template created by Serverpod, edit `notes_server/lib/src/greeting.spy.yaml`: @@ -379,8 +382,6 @@ docker compose up --detach dart run bin/main.dart ``` -## Data sync - 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: