Skip to main content

Motivation

PowerSync’s original Sync Rules system was optimized for offline-first use cases where you want to “sync everything upfront” when the client connects, so that data is available locally if a user goes offline at any point. However, many developers are building apps where users are mostly online, and you don’t want to make users wait to sync a lot of data upfront. In these cases, it’s more suited to sync data on-demand. This is especially true for web apps: users are mostly online and you often want to sync only the data needed for the current page. Users also frequently have multiple tabs open, each needing different subsets of data. Sync engines like PowerSync are still great for these online web app use cases, because they provide you with real-time updates, simplified state management, and ease of working with data locally. Client Parameters in the current Sync Rules system support on-demand syncing across different browser tabs to some extent: For example, using a project_ids array as a Client Parameter to sync only specific projects. However, manually managing these arrays across different browser tabs becomes quite painful. We are introducing Sync Streams to provide the best of both worlds: support for dynamic on-demand syncing, as well as “syncing everything upfront”. Key improvements in Sync Streams over Sync Rules include:
  1. On-demand syncing: You define Sync Streams on the PowerSync Service, and a client can then subscribe to them one or more times with different parameters.
  2. Temporary caching-like behavior: Each subscription includes a configurable ttl that keeps data active after your app unsubscribes, acting as a warm cache for recently accessed data.
  3. Simpler developer experience: Simplified syntax and mental model, and capabilities such as your UI components automatically managing subscriptions (for example, React hooks).
If you want “sync everything upfront” behavior (like the current Sync Rules system), that’s easy too: you can configure any of your Sync Streams to be auto-subscribed by the client on connecting.
Early Alpha ReleaseSync Streams will ultimately replace the current Sync Rules system. They are currently in an early alpha release, which of course means they’re not yet suitable for production use, and the APIs and DX likely still need refinement.They are open for anyone to test: we are actively seeking your feedback on their performance for your use cases, the developer experience, missing capabilities, and potential optimizations. Please share your feedback with us in Discord 🫡Sync Streams will be supported alongside Sync Rules for the foreseeable future, although we recommend migrating to Sync Streams once in Beta.

Requirements for Using Sync Streams

  • v1.15.0 of the PowerSync Service (Cloud instances are already on this version)
  • Minimum SDK versions:
    • JS:
      • Web: v1.27.0
      • React Native: v1.25.0
      • React hooks: v1.8.0
    • Dart: v1.16.0
    • Kotlin: v1.7.0
    • Swift: Coming soon.
  • Use of the Rust-based sync client
  • Sync Stream definitions. They are currently defined in the same YAML file as Sync Rules: sync_rules.yaml (PowerSync Cloud) or config.yaml (Open Edition/self-hosted). To enable Sync Streams, add the following configuration:
    sync_rules.yaml
    config:
      # see https://docs.powersync.com/usage/sync-rules/compatibility
      # this edition also deploys several backwards-incompatible fixes
      # see the docs for details
      edition: 2
    
    streams:
      ... # see 'Stream Definition Syntax' section below
    

Stream Definition Syntax

You specify stream definitions similar to bucket definitions in Sync Rules. Clients then subscribe to the defined streams one or more times, with different parameters. Syntax:
sync_rules.yaml
streams:
  <stream_name>:
    query: string # similar to Data Queries in Sync Rules, but also support limited subqueries.
    auto_subscribe: boolean # true to subscribe to this stream by default (similar to how Sync Rules work), false (default) if clients should explicitly subscribe.
    priority: number # sync priority, same as in Sync Rules: https://docs.powersync.com/usage/use-case-examples/prioritized-sync
    accept_potentially_dangerous_queries: boolean # silence warnings on dangerous queries, same as in Sync Rules.
Basic example:
sync_rules.yaml
config:
  edition: 2
streams:
  issue: # Define a stream to a specific issue
    query: select * from issues where id = subscription.parameters() ->> 'id' 
  issue_comments: # Define a stream to a specific issue's comments
    query: select * from comments where issue_id = subscription.parameters() ->> 'id' 

Just Queries with Subqueries

Whereas Sync Rules had separate Parameter Queries and Data Queries, Sync Streams only have a query. Instead of Parameter Queries, Sync Streams can use parameters directly in the query, and support a limited form of subqueries. For example:
sync_rules.yaml
# use parameters directly in the query (see below for details on accessing parameters)
select * from issues where id = subscription.parameters() ->> 'id' and owner_id = auth.user_id()

# "in (subquery)" replaces parameter queries:
select * from comments where issue_id in (select id from issues where owner_id = auth.user_id())
Under the hood, Sync Streams use the same bucket system as Sync Rules, so you get the same functionality as before with Parameter Queries, however, the Sync Streams syntax is closer to plain SQL.

Accessing Parameters

We have streamlined how different kinds of parameters are accessed in Sync Streams compared to Sync Rules. Subscription Parameters: Passed from the client when it subscribes to a Sync Stream. See Client-Side Syntax below. Clients can subscribe to the same stream multiple times with different parameters:
subscription.parameters() # all parameters for the subscription, as JSON
subscription.parameter('key') # shorthand for getting a single specific parameter
Auth Parameters: Claims from the JWT:
auth.parameters() # JWT token payload, as JSON
auth.parameter('key') # short-hand for getting a single specific token payload parameter
auth.user_id() # same as auth.parameter('sub')
Connection Parameters: Specified “globally” on the connection level. These are the equivalent of Client Parameters in Sync Rules:
connection.parameters() # all parameters for the connection, as JSON
connection.parameter('key') # shorthand for getting a single specific parameter

Usage Examples: Sync Rules vs Sync Streams

Client-Side Syntax

In general, each SDK lets you:
  • Use db.syncStream(name, [subscription-params]) to get a SyncStream instance.
  • Call subscribe() on a SyncStream to get a SyncStreamSubscription. This gives you access to waitForFirstSync() and unsubscribe().
  • Inspect SyncStatus for a list of SyncSubscriptionDefinitions describing all Sync Streams your app is subscribed to (either due to an explicit subscription or because the Sync Stream has auto_subscribe: true). It also reports per-stream download progress.
  • Each Sync Stream has a ttl (time-to-live). After you call unsubscribe(), or when the page/app closes, the stream keeps syncing for the ttl duration, enabling caching-like behavior. Each SDK lets you specify the ttl, or ignore the ttl and delete the data as soon as possible. If not specified, a default TTL of 24 hours applies.
Select your language for specific examples:
  • JS
  • Dart
  • Kotlin
  • Swift - Coming soon
const sub = await powerSync.syncStream('issues', {id: 'issue-id'}).subscribe(ttl: 3600);

// Resolve current status for subscription
const status = powerSync.currentStatus.forStream(sub);
const progress = status?.progress;

// Wait for this subscription to have synced
await sub.waitForFirstSync();

// When the component needing the subscription is no longer active...
sub.unsubscribe();
If you’re using React, you can also use hooks to automatically subscribe components to Sync Streams:
const stream = useSyncStream({ name: 'todo_list', parameters: { list: 'foo' } });
// Can then check for download progress or subscription information
stream?.progress;
stream?.subscription.hasSynced;
This hook is useful when you want to explicitly ensure a stream is active (for example a root component) or when you need progress/hasSynced state; this makes data available for all child components without each query declaring the stream.Additionally, the useQuery hook for React can wait for Sync Streams to be complete before running queries. Pass streams only when the component knows which specific stream subscription(s) it depends on and it should wait before querying.
const results = useQuery(
  'SELECT ...',
  queryParameters,
  // This will wait for the stream to sync before running the query
  { streams: [{ name: 'todo_list', parameters: { list: 'foo' }, waitForStream: true }] }
);

Example

Example apps are still in progress. You can follow the below PRs or use them as a reference.

Dart

You can try the supabase-todolist demo app, which we updated to use Sync Streams (Sync Rules are still supported). Use the following Sync Stream definitions on the PowerSync Service:
sync_rules.yaml
config:
  edition: 2
streams:
  lists:
    query: SELECT * FROM lists
    auto_subscribe: true
  todos:
    query: SELECT * FROM todos WHERE list_id = subscription.parameter('list')
In this example:
  • The app syncs lists by default (demonstrating equivalent behavior to Sync Rules, i.e. optimized for offline-first).
  • The app syncs todos on demand when a user opens a list.
  • When the user navigates back to the same list, they won’t see a loading state — demonstrating caching behavior.

JS

https://github.com/powersync-ja/powersync-js/pull/721

Kotlin

https://github.com/powersync-ja/powersync-kotlin/pull/270