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:
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.
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.
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.
You can migrate back to the JavaScript client later by removing the option.
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
Copy
config: # see https://docs.powersync.com/usage/sync-rules/compatibility # this edition also deploys several backwards-incompatible fixes # see the docs for details edition: 2streams: ... # see 'Stream Definition Syntax' section below
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
Copy
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
Copy
config: edition: 2streams: 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'
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
Copy
# 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.
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:
Copy
subscription.parameters() # all parameters for the subscription, as JSONsubscription.parameter('key') # shorthand for getting a single specific parameter
Auth Parameters: Claims from the JWT:
Copy
auth.parameters() # JWT token payload, as JSONauth.parameter('key') # short-hand for getting a single specific token payload parameterauth.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:
Copy
connection.parameters() # all parameters for the connection, as JSONconnection.parameter('key') # shorthand for getting a single specific parameter
bucket_definitions: global: data: # Sync all todos - SELECT * FROM todos # Sync all lists except archived ones - SELECT * FROM lists WHERE archived = false
Sync Streams: “Global” data — the data you want all of your users to have by default — is also defined as streams. Specify auto_subscribe: true so your users subscribe to them by default.
sync_rules.yaml
Copy
streams: all_todos: query: SELECT * FROM todos auto_subscribe: true unarchived_lists: query: SELECT * FROM lists WHERE archived = false auto_subscribe: true
bucket_definitions: owned_lists: parameters: | SELECT id as list_id FROM lists WHERE owner_id = request.user_id() data: - SELECT * FROM lists WHERE lists.id = bucket.list_id - SELECT * FROM todos WHERE todos.list_id = bucket.list_id
Sync Streams:
sync_rules.yaml
Copy
streams: owned_lists: query: SELECT * FROM lists WHERE owner_id = auth.user_id() list_todos: query: SELECT * FROM todos WHERE list_id = subscription.parameter('list_id') AND list_id IN (SELECT id FROM lists WHERE owner_id = auth.user_id())
bucket_definitions: posts: parameters: SELECT (request.parameters() ->> 'current_page') as page_number data: - SELECT * FROM posts WHERE page_number = bucket.page_number
Sync Streams:
sync_rules.yaml
Copy
streams: posts: query: SELECT * FROM posts WHERE page_number = subscription.parameter('page_number')
Note that the behavior here is different to Sync Rules because subscription.parameter('page_number') is local to the subscription, so the Sync Stream can be subscribed to multiple times with different page numbers, whereas Sync Rules only allow a single global Client Parameter value at a time. Connection Parameters (connection.parameter()) are available in Sync Streams as the equivalent of the global Client Parameters in Sync Rules, but Subscription Parameters are recommended because they are much more flexible.
Specific columns/fields, renames and transformations
Selecting, renaming or transforming specific columns/fields is identical between Sync Rules and Sync Streams:
sync_rules.yaml
Copy
streams: todos: # Select specific columns query: SELECT id, name, owner_id FROM todos # Rename columns query: SELECT id, name, created_timestamp AS created_at FROM todos # Cast number to text query: SELECT id, item_number :: text AS item_number FROM todos # Alternative syntax for the same cast query: id, CAST(item_number as TEXT) AS item_number FROM todos # Convert binary data (bytea) to base64 query: id, base64(thumbnail) AS thumbnail_base64 FROM todos # Extract field from JSON or JSONB column query: id, metadata_json ->> 'description' AS description FROM todos # Convert time to epoch number query: id, unixepoch(created_at) AS created_at FROM todos
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
Copy
const sub = await powerSync.syncStream('issues', {id: 'issue-id'}).subscribe(ttl: 3600);// Resolve current status for subscriptionconst status = powerSync.currentStatus.forStream(sub);const progress = status?.progress;// Wait for this subscription to have syncedawait 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:
Copy
const stream = useSyncStream({ name: 'todo_list', parameters: { list: 'foo' } });// Can then check for download progress or subscription informationstream?.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.
Copy
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 }] });
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
Copy
config: edition: 2streams: 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.