Skip to main content

Why Migrate?

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 data is available locally if the user goes offline. 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. This is especially true for web apps: users are mostly online, you often want to sync only the data needed for the current page, and users frequently have multiple browser tabs open — each needing different subsets of data.

The Problem with Client Parameters

Client Parameters in Sync Rules partially support on-demand syncing — for example, using a project_ids array to sync only specific projects. However, manually managing these arrays across different browser tabs becomes painful:
  • You need to aggregate IDs across all open tabs
  • You need additional logic for different data types (tables)
  • If you want to keep data around after a tab closes (caching), you need even more management

How Sync Streams Solve This

Sync Streams address these limitations:
  1. On-demand syncing: Define streams once, then subscribe from your app one or more times with different parameters. No need to manage arrays of IDs — each subscription is independent.
  2. Multi-tab support: Each subscription manages its own lifecycle. Open the same list in two tabs? Each tab subscribes independently. Close one? The other keeps working.
  3. Built-in caching: Each subscription has a configurable ttl that keeps data cached after unsubscribing. When users return to a screen, data may already be available — no loading state needed.
  4. Simpler, more powerful syntax: Queries with subqueries, JOINs, and CTEs. No separate parameter queries. The syntax is closer to plain SQL and supports more SQL features than Sync Rules.
  5. Framework integration: React hooks and Kotlin Compose extensions let your UI components automatically manage subscriptions based on what’s rendered.

Still Need Offline-First?

If you want “sync everything upfront” behavior (like Sync Rules), set auto_subscribe: true on your Sync Streams and clients will subscribe automatically when they connect.

Requirements

  • PowerSync Service v1.20.0+ (Cloud instances already meet this)
  • Latest SDK versions with Rust-based sync client (enabled by default on latest SDKs)
  • config: edition: 3 in your sync config
SDKMinimum VersionRust Client Default
JS Webv1.27.0v1.32.0
React Nativev1.25.0v1.29.0
React hooksv1.8.0
Node.jsv0.11.0v0.16.0
Capacitorv0.0.1v0.3.0
Dart/Flutterv1.16.0v1.17.0
Kotlinv1.7.0v1.9.0
Swiftv1.11.0v1.8.0
.NETv0.0.8-alpha.1v0.0.5-alpha.1

Migration Tool

You can generate a Sync Streams draft from your existing Sync Rules in two ways:
  1. Dashboard: In the PowerSync Dashboard, use the Migrate to Sync Streams button. It converts your Sync Rules into a Sync Streams draft that you can review before deploying.
  2. CLI: Run powersync migrate sync-rules to produce a Sync Streams draft from your current sync config.
A standalone migration tool is also available here.
The output uses auto_subscribe: true by default, preserving your existing sync-everything-upfront behavior so no client-side changes are required when you first deploy. Next steps: Review the draft, then deploy it (via the Dashboard or powersync deploy sync-config). After that, you can optionally migrate individual streams to on-demand subscriptions over time — remove auto_subscribe: true from specific streams and update client code to use the syncStream() API where it makes sense for your app.

Stream Definition Reference

config:
  edition: 3

streams:
  <stream_name>:
    # CTEs (optional) - define with block inside each stream
    with:
      <cte_name>: SELECT ... FROM ...

    # Behavior options (place above query/queries)
    auto_subscribe: true    # Auto-subscribe clients on connect (default: false)
    priority: 1             # Sync priority (optional). Lower number -> higher priority
    accept_potentially_dangerous_queries: true  # Silence security warnings (default: false)

    # Query options (use one)
    query: SELECT * FROM <table> WHERE ...         # Single query
    queries:                                       # Multiple queries
      - SELECT * FROM <table_a> WHERE ...
      - SELECT * FROM <table_b> WHERE ...

    
OptionDefaultDescription
querySQL-like query defining which data to sync. Use either query or queries, not both. See Writing Queries.
queriesArray of queries defining which data to sync. More efficient than defining separate streams: the client manages one subscription and PowerSync merges the data from all queries (see Multiple Queries per Stream).
withCTEs available to this stream’s queries. Define the with block inside each stream.
auto_subscribefalseWhen true, clients automatically subscribe on connect.
prioritySync priority (lower value = higher priority). See Prioritized Sync.
accept_potentially_dangerous_queriesfalseSilences security warnings when queries use client-controlled parameters (i.e. connection parameters and subscription parameters), as opposed to authentication parameters that are signed as part of the JWT. Set to true only if you’ve verified the query is safe. See Using Parameters.

Migration Examples

Global Data (No Parameters)

In Sync Rules, a “global” bucket syncs the same data to all users. In Sync Streams, you achieve this with queries that have no parameters. Add auto_subscribe: true to maintain the Sync Rules behavior where data syncs automatically on connect. Sync Rules:
bucket_definitions:
  global:
    data:
      - SELECT * FROM todos
      - SELECT * FROM lists WHERE archived = false
Sync Streams:
config:
  edition: 3

streams:
  shared_data:
    auto_subscribe: true  # Sync automatically like Sync Rules
    queries:
      - SELECT * FROM todos
      - SELECT * FROM lists WHERE archived = false
Without auto_subscribe: true, clients would need to explicitly subscribe to these streams. This gives you flexibility to migrate incrementally or switch to on-demand syncing later.

User-Scoped Data

Sync Rules:
bucket_definitions:
  user_lists:
    priority: 1
    parameters: SELECT request.user_id() as user_id
    data:
      - SELECT * FROM lists WHERE owner_id = bucket.user_id
Sync Streams:
config:
  edition: 3

streams:
  user_lists:
    auto_subscribe: true
    priority: 1
    query: SELECT * FROM lists WHERE owner_id = auth.user_id()

Data with Subqueries (Replaces Parameter Queries)

Sync Rules:
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:
config:
  edition: 3

streams:
  owned_lists:
    auto_subscribe: true
    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())

Client Parameters → Subscription Parameters

Sync Rules used global Client Parameters:
bucket_definitions:
  posts:
    parameters: SELECT (request.parameters() ->> 'current_page') as page_number
    data:
      - SELECT * FROM posts WHERE page_number = bucket.page_number
Sync Streams use Subscription Parameters, which are more flexible — you can subscribe multiple times with different values:
config:
  edition: 3

streams:
  posts:
    query: SELECT * FROM posts WHERE page_number = subscription.parameter('page_number')
// Subscribe to multiple pages simultaneously
const page1 = await db.syncStream('posts', { page_number: 1 }).subscribe();
const page2 = await db.syncStream('posts', { page_number: 2 }).subscribe();

Parameter Syntax Changes

Sync RulesSync Streams
request.user_id()auth.user_id()
request.jwt() ->> 'claim'auth.parameter('claim')
request.parameters() ->> 'key'subscription.parameter('key') (subscription parameter) or connection.parameter('key') (connection parameter)
bucket.param_nameUse the parameter directly in the query e.g. subscription.parameter('key')

Client-Side Changes

After updating your sync config, update your client code to use subscriptions:
// Before (Sync Rules with Client Parameters)
await db.connect(connector, {
  params: { current_project: projectId }
});

// After (Sync Streams with Subscriptions)
await db.connect(connector);
const sub = await db.syncStream('project_data', { project_id: projectId }).subscribe();
See Client-Side Usage for detailed examples.