> ## Documentation Index
> Fetch the complete documentation index at: https://docs.powersync.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sync Streams

> Introduction to Sync Streams, the recommended way to define which data syncs to each client with SQL-based stream definitions.

Instead of syncing entire tables, you tell PowerSync exactly which data each user/client can sync. You write simple SQL-like queries to define streams of data, and your client app subscribes to the streams it needs. PowerSync handles the rest, keeping data in sync in real-time and making it available offline.

For example, you might create a stream that syncs only the current user's to-do items, another for shared projects they have access to, and another for reference data that everyone needs. Your app subscribes to these streams on demand, and only that data syncs to the client-side SQLite database. Offline-first apps that need all relevant data available upfront can use `auto_subscribe: true` so streams sync automatically when clients connect.

<Note>
  **Are you still using Sync Rules?** Sync Streams support everything Sync Rules do, plus more expressive queries (including JOIN support), on-demand syncing, and a simpler developer experience (e.g. React hooks that manage subscriptions automatically).

  You can migrate in a few clicks. Click **Migrate to Sync Streams** in the PowerSync Dashboard, or run `powersync migrate sync-rules` in the CLI to generate a draft from your current config. See the [migration guide](/sync/streams/migration) for details.
</Note>

## Defining Streams

Streams are defined in a <Tooltip tip="For PowerSync Cloud, the YAML file is edited and deployed to a specific PowerSync instance in the PowerSync Dashboard. For self-hosting setups, the YAML is specified in your instance configuration (either inline or referencing a file).">YAML</Tooltip> configuration file. Each stream has a **name** and a **query** that specifies which rows to sync using SQL-like syntax. The query can reference [parameters](/sync/overview#how-it-works) like the authenticated user's ID to personalize what each user receives.

<Tabs>
  <Tab title="PowerSync Cloud">
    In the [PowerSync Dashboard](https://dashboard.powersync.com/):

    1. Select your project and instance
    2. Go to **Sync Streams**
    3. Edit the YAML directly in the dashboard
    4. Click **Deploy** to validate and deploy

    ```yaml theme={null}
    config:
      edition: 3

    streams:
      todos:
        query: SELECT * FROM todos WHERE owner_id = auth.user_id()
    ```
  </Tab>

  <Tab title="Self-Hosted">
    Add a `sync_config` section to your `service.yaml`. Using a **separate file** is recommended (e.g. `sync_config: path: sync-config.yaml`). Put the stream definition in that file:

    ```yaml sync-config.yaml theme={null}
    config:
      edition: 3

    streams:
      todos:
        query: SELECT * FROM todos WHERE owner_id = auth.user_id()
    ```

    You can also use inline `sync_config: content: |` with the YAML nested in your main config. See [Self-Hosted Instance Configuration](/configuration/powersync-service/self-hosted-instances#sync-streams--sync-rules) for both options.
  </Tab>
</Tabs>

Available stream options:

```yaml theme={null}
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 ...

    
```

| Option                                 | Default | Description                                                                                                                                                                                                                                                                                                                     |
| -------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `query`                                | —       | SQL-like query defining which data to sync. Use either `query` or `queries`, not both. See [Writing Queries](/sync/streams/queries).                                                                                                                                                                                            |
| `queries`                              | —       | Array 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](/sync/streams/queries#multiple-queries-per-stream)).                                                         |
| `with`                                 | —       | [CTEs](/sync/streams/ctes) available to this stream's queries. Define the `with` block inside each stream.                                                                                                                                                                                                                      |
| `auto_subscribe`                       | `false` | When `true`, clients automatically subscribe on connect.                                                                                                                                                                                                                                                                        |
| `priority`                             | —       | Sync priority (lower value = higher priority). See [Prioritized Sync](/sync/advanced/prioritized-sync).                                                                                                                                                                                                                         |
| `accept_potentially_dangerous_queries` | `false` | Silences 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](/sync/streams/parameters). |

## Basic Examples

There are two independent concepts to understand:

* *What* data the stream returns. For example:
  * *Global data*: No parameters. Same data for all users (e.g. reference tables like categories).
  * *Filtered data*: Filters the data by a parameter value. This can make use of *auth parameters* from the JWT token (such as the user ID or other JWT claims), *subscription parameters* (specified by the client when it subscribes to a stream at any time), or *connection parameters* (specified at connection). Different users will get different sets of data based on the parameters. See [Using Parameters](/sync/streams/parameters) for the full reference.
* *When* the client syncs the data
  * *Auto-subscribe*: Client automatically subscribes on connect (`auto_subscribe: true`)
  * *On-demand*: Client explicitly subscribes when needed (default behavior)

### Global Data

Data without parameters is "global" data, meaning the same data goes to all users/clients. This is useful for reference tables:

```yaml theme={null}
config:
  edition: 3

streams:
  # Same categories for everyone
  categories:
    query: SELECT * FROM categories

  # Same active products for everyone
  products:
    query: SELECT * FROM products WHERE active = true
```

<Note>
  Global data streams still require clients to subscribe explicitly unless you set `auto_subscribe: true`
</Note>

### Filtering Data by User

Use `auth.user_id()` or other [JWT claims](/sync/streams/parameters#auth-parameters) to return different data per user:

```yaml theme={null}
config:
  edition: 3

streams:
  # Each user gets their own lists
  my_lists:
    query: SELECT * FROM lists WHERE owner_id = auth.user_id()

  # Each user gets their own orders
  my_orders:
    query: SELECT * FROM orders WHERE user_id = auth.user_id()
```

### Filtering Data Based on Subscription Parameters

Use `subscription.parameter()` for data that clients subscribe to explicitly:

```yaml theme={null}
config:
  edition: 3

streams:
  # Sync todos for a specific list when the client subscribes with a list_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())
```

```js theme={null}
// Client subscribes with the list they want to view
const sub = await db.syncStream('list_todos', { list_id: 'abc123' }).subscribe();
```

### Using Auto-Subscribe

Set `auto_subscribe: true` to sync data automatically when clients connect. This is useful for:

* Reference data that all users need, or that are needed in many screens in the app.
* User data that should always be available offline
* Maintaining [Sync Rules](/sync/rules/overview) default behavior ("sync everything upfront") when migrating to Sync Streams

```yaml theme={null}
config:
  edition: 3

streams:
  # Global data, synced automatically
  categories:
    auto_subscribe: true
    query: SELECT * FROM categories

  # User-scoped data, synced automatically
  my_orders:
    auto_subscribe: true
    query: SELECT * FROM orders WHERE user_id = auth.user_id()

  # Parameterized data, subscribed on-demand (no auto_subscribe)
  order_items:
    query: |
      SELECT * FROM order_items 
      WHERE order_id = subscription.parameter('order_id')
        AND order_id IN (SELECT id FROM orders WHERE user_id = auth.user_id())
```

## Client-Side Usage

Subscribe to streams from your client app:

<Tabs>
  <Tab title="JavaScript/TypeScript">
    ```js theme={null}
    const sub = await db.syncStream('list_todos', { list_id: 'abc123' })
      .subscribe({ ttl: 3600 });

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

    // When the component needing the subscription is no longer active...
    sub.unsubscribe();
    ```

    **React hooks:**

    ```jsx theme={null}
    const stream = useSyncStream({ name: 'list_todos', parameters: { list_id: 'abc123' } });
    // Check download progress or subscription information
    stream?.progress;
    stream?.subscription.hasSynced;
    ```

    The `useQuery` hook can wait for Sync Streams before running queries:

    ```jsx theme={null}
    const { data } = useQuery(
      'SELECT * FROM todos WHERE list_id = ?',
      [listId],
      { streams: [{ name: 'list_todos', parameters: { list_id: listId }, waitForStream: true }] }
    );
    ```
  </Tab>

  <Tab title="Dart">
    ```dart theme={null}
    final sub = await db
      .syncStream('list_todos', {'list_id': 'abc123'})
      .subscribe(ttl: const Duration(hours: 1));

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

    // When the component needing the subscription is no longer active...
    sub.unsubscribe();
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    val sub = database.syncStream("list_todos", mapOf("list_id" to JsonParam.String("abc123")))
      .subscribe(ttl = 1.0.hours)

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

    // When the component needing the subscription is no longer active...
    sub.unsubscribe()
    ```
  </Tab>

  <Tab title="Swift">
    ```swift theme={null}
    let sub = try await db.syncStream(name: "list_todos", params: ["list_id": JsonValue.string("abc123")])
      .subscribe(ttl: 60 * 60, priority: nil) // 1 hour

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

    // When the component needing the subscription is no longer active...
    try await sub.unsubscribe()
    ```
  </Tab>

  <Tab title=".NET">
    ```csharp theme={null}
    var sub = await db.SyncStream("list_todos", new() { ["list_id"] = "abc123" })
      .Subscribe(new SyncStreamSubscribeOptions { Ttl = TimeSpan.FromHours(1) });

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

    // When the component needing the subscription is no longer active...
    sub.Unsubscribe();
    ```
  </Tab>
</Tabs>

### TTL (Time-To-Live)

Each subscription has a `ttl` that keeps data cached after unsubscribing. This enables warm cache behavior — when users return to a screen and you re-subscribe to relevant streams, data is already available on the client. Default TTL is 24 hours. See [Client-Side Usage](/sync/streams/client-usage) for details.

```js theme={null}
// Set TTL in seconds when subscribing
const sub = await db.syncStream('todos', { list_id: 'abc' })
  .subscribe({ ttl: 3600 }); // Cache for 1 hour after unsubscribe
```

## Developer Notes

* **SQL Syntax**: Stream queries use a SQL-like syntax with `SELECT` statements. You can use subqueries, `INNER JOIN`, and [CTEs](/sync/streams/ctes) for filtering. `GROUP BY`, `ORDER BY`, and `LIMIT` are not supported. See [Writing Queries](/sync/streams/queries) for details on joins, multiple queries per stream, and other features.

* **Type Conversion**: Data types from your source database (Postgres, MongoDB, MySQL, SQL Server) are converted when synced to the client's SQLite database. SQLite has a limited type system, so most types become `text` and you may need to parse or cast values in your app code. See [Type Mapping](/sync/types) for details on how each type is handled.

* **Primary Key**: PowerSync requires every synced table to have a primary key column named `id` of type `text`. If your backend uses a different column name or type, you'll need to map it. For MongoDB, collections use `_id` as the ID field; you must alias it in your stream queries (e.g. `SELECT _id as id, * FROM your_collection`).

* **Case Sensitivity**: To avoid issues across different databases and platforms, use **lowercase identifiers** for all table and column names in your Sync Streams. If your backend uses mixed case, see [Case Sensitivity](/sync/advanced/case-sensitivity) for how to handle it.

* **Bucket Limits**: PowerSync uses internal partitions called [buckets](/architecture/powersync-service#bucket-system) to efficiently sync data. There's a default [limit of 1,000 buckets](/resources/performance-and-limits) per user/client. Each stream creates one bucket per unique value of its filter expression, whether from a subquery, JOIN, auth parameter, or subscription parameter.  You can use [multiple queries per stream](/sync/streams/queries#multiple-queries-per-stream) and other strategies to reduce bucket count. See [Too Many Buckets](/debugging/troubleshooting#too-many-buckets-psync_s2305) in the troubleshooting guide for how to diagnose and resolve high bucket count (`PSYNC_S2305` errors).

* **Troubleshooting**: If data isn't syncing as expected, the [Sync Diagnostics Client](/tools/diagnostics-client) helps you inspect what's happening for a specific user — you can see which buckets the user has and what data is being synced.

## Examples & Demos

See [Examples & Demos](/sync/streams/examples) for working demo apps and complete application patterns.

## Migrating from Legacy Sync Rules

If you have an existing project using legacy Sync Rules, see the [Migration Guide](/sync/streams/migration) for step-by-step instructions, syntax changes, and examples.
