> ## 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.

# Client-Side Usage

> Subscribe to Sync Streams from your client app and manage subscriptions dynamically.

After [defining your streams](/sync/streams/overview#defining-streams) on the server-side, your client app subscribes to them to start syncing data (this is an explicit operation unless streams are configured to [auto-subscribe](/sync/streams/overview#using-auto-subscribe)). This page covers everything you need to use Sync Streams from your client code.

## Quick Start

Streams that are configured to [auto-subscribe](/sync/streams/overview#using-auto-subscribe) will automatically start syncing as soon as you connect to your PowerSync instance in your client-side application.

For any other streams, the basic pattern is: **subscribe** to a stream, **wait** for data to sync, then **unsubscribe** when done.

<Tabs>
  <Tab title="TypeScript/JavaScript">
    <Note>
      **Tauri SDK**: The JavaScript API shown in the TypeScript/JavaScript tabs throughout this page applies to Tauri as well. Import from `@powersync/tauri-plugin` (for `PowerSyncTauriDatabase`) or `@powersync/common` (for shared types). See the [Connection Parameters](#connection-parameters) section below for how Tauri handles connect-time parameters differently.
    </Note>

    ```js theme={null}
    // Subscribe to a stream with parameters
    const sub = await db.syncStream('list_todos', { list_id: 'abc123' }).subscribe();

    // Wait for initial data to sync
    await sub.waitForFirstSync();

    // Your data is now available - query it normally
    const todos = await db.getAll('SELECT * FROM todos WHERE list_id = ?', ['abc123']);

    // When leaving the screen or component...
    sub.unsubscribe();
    ```
  </Tab>

  <Tab title="Dart">
    ```dart theme={null}
    // Subscribe to a stream with parameters
    final sub = await db.syncStream('list_todos', {'list_id': 'abc123'}).subscribe();

    // Wait for initial data to sync
    await sub.waitForFirstSync();

    // Your data is now available - query it normally
    final todos = await db.getAll('SELECT * FROM todos WHERE list_id = ?', ['abc123']);

    // When leaving the screen or component...
    sub.unsubscribe();
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    // Subscribe to a stream with parameters
    val sub = database.syncStream("list_todos", mapOf("list_id" to JsonParam.String("abc123")))
      .subscribe()

    // Wait for initial data to sync
    sub.waitForFirstSync()

    // Your data is now available - query it normally
    val todos = database.getAll("SELECT * FROM todos WHERE list_id = ?", listOf("abc123"))

    // When leaving the screen or component...
    sub.unsubscribe()
    ```
  </Tab>

  <Tab title="Swift">
    ```swift theme={null}
    // Subscribe to a stream with parameters
    let sub = try await db.syncStream(name: "list_todos", params: ["list_id": JsonValue.string("abc123")]).subscribe()

    // Wait for initial data to sync
    try await sub.waitForFirstSync()

    // Your data is now available - query it normally
    let todos = try await db.getAll(sql: "SELECT * FROM todos WHERE list_id = ?", parameters: ["abc123"])

    // When leaving the screen or component...
    try await sub.unsubscribe()
    ```
  </Tab>

  <Tab title=".NET">
    ```csharp theme={null}
    // Subscribe to a stream with parameters
    var sub = await db.SyncStream("list_todos", new() { ["list_id"] = "abc123" }).Subscribe();

    // Wait for initial data to sync
    await sub.WaitForFirstSync();

    // Your data is now available - query it normally
    var todos = await db.GetAll<Todo>("SELECT * FROM todos WHERE list_id = ?", new[] { "abc123" });

    // When leaving the screen or component...
    sub.Unsubscribe();
    ```
  </Tab>
</Tabs>

## Framework Integrations

Most developers use framework-specific hooks that handle subscription lifecycle automatically.

<Tabs>
  <Tab title="React Hooks">
    The PowerSync React package provides three hooks relevant to Sync Stream subscriptions. Each serves a distinct role:

    * **`useQuery`**: queries the local SQLite database and returns live-updating results. It also accepts a `streams` option to subscribe to streams alongside the query — use this when the stream is only needed for that specific query.
    * **`useSyncStream`**: subscribes to a single named Sync Stream. Use this when the same stream's data is needed across multiple queries or components; the subscription lives independently of any particular `useQuery` call and is cancelled when the component unmounts.
    * **`useSyncStreams`**: the same as `useSyncStream` but for a variable number of streams. Accepts an array of stream options; all subscriptions are cancelled when the component unmounts or the array changes.

    ### `useQuery` with `streams`: Tethering a Subscription to a Query

    `useQuery` accepts an optional `streams` option that subscribes to one or more streams and ties their lifecycle to that specific query. This is the simplest option when the stream is only ever needed alongside this one query:

    ```jsx theme={null}
    import { useQuery } from '@powersync/react';

    function TodoList({ listId }) {
      // The stream subscription is created alongside this query and cancelled with it.
      const { data: todos } = useQuery(
        'SELECT * FROM todos WHERE list_id = ?',
        [listId],
        { streams: [{ name: 'list_todos', parameters: { list_id: listId }, waitForStream: true }] }
      );

      return <TodoItems todos={todos} />;
    }
    ```

    ### `useSyncStream`: Single Stream

    Use this when a component needs data from exactly one stream.

    ```jsx theme={null}
    import { useSyncStream, useQuery } from '@powersync/react';

    function TodoList({ listId }) {
      // Subscribe to the stream for this list. Unsubscribes when component unmounts.
      const stream = useSyncStream({ name: 'list_todos', parameters: { list_id: listId } });

      if (!stream?.subscription.hasSynced) {
        return <LoadingSpinner />;
      }

      // Data is synced — read it from the local database
      const { data: todos } = useQuery('SELECT * FROM todos WHERE list_id = ?', [listId]);
      return <TodoItems todos={todos} />;
    }
    ```

    ### `useSyncStreams`: Variable Number of Streams

    Use this when the number of streams is only known at runtime — for example, when it depends on an array from props or state:

    ```jsx theme={null}
    import { useSyncStreams } from '@powersync/react';

    function ProjectView({ listIds }) {
      // Subscribe to one stream per list. All subscriptions are managed together.
      const statuses = useSyncStreams(
        listIds.map((id) => ({ name: 'list_todos', parameters: { list_id: id } }))
      );

      const allSynced = statuses.every((s) => s?.subscription?.hasSynced);

      if (!allSynced) {
        return <LoadingSpinner />;
      }

      return <TodoLists listIds={listIds} />;
    }
    ```
  </Tab>

  <Tab title="TanStack Query">
    Both the `useQuery` and `useQueries` hooks automatically subscribe when the component mounts and will unsubscribe when it unmounts:

    ```jsx theme={null}
     function TodoList({ listId }) {
       // Automatically subscribes/unsubscribes based on component lifecycle
       const stream = useSyncStream({ name: 'list_todos', parameters: { list_id: listId } });
       const { data: todos, isLoading } = useQuery({
         queryKey: ['test'],
         query: 'SELECT 1',
         streams: [{ name: 'list_todos', parameters: { list_id: listId }, waitForStream: true }]
       });
       
       // Check if data has synced
       if (isLoading) {
         return <LoadingSpinner />;
       }
       
       // Data is ready - query and render
       return <TodoItems todos={todos} />;
     }
    ```

    ```jsx theme={null}
    function TodoList({ listId }) {
      // Automatically subscribes/unsubscribes based on component lifecycle
      const { allData, anyPending} = useQueries({
        queries: [
          { queryKey: ['test1'], query: 'SELECT 1', streams: [{ name: 'a' }] },
          { queryKey: ['test2'], query: 'SELECT 2' }
        ],
        combine: (results) => ({
          allData: results.map((r) => r.data),
          anyPending: results.some((r) => r.isPending)
        })
      })}
      ...
    }
    ```
  </Tab>

  <Tab title="Vue/Nuxt">
    The `useSyncStream` composable automatically subscribes when the component mounts and unsubscribes when it unmounts:

    ```vue theme={null}
    <script setup>
     import { useQuery, useSyncStream } from '@powersync/vue'
     import { computed } from 'vue'

     // props
     const props = defineProps({
       listId: {
         type: String,
         required: true
       }
     })

     // Automatically subscribes/unsubscribes with component lifecycle
     const stream = useSyncStream(
       'list_todos',
       {
         parameters: {
           list_id: props.listId

         }
       })

     // Run query once synced
     const todosQuery = useQuery(
       'SELECT * FROM todos WHERE list_id = ?',
       [props.listId]
     )

     // derived state
     const loading = computed(() => stream.status?.value?.subscription)
     const todos = computed(() => todosQuery.data)
    </script>
    ```

    You can also have `useQuery` wait for a stream before running:

    ```Javascript theme={null}
    // This query waits for the stream to sync before executing
    const { data: todos } = useQuery(
      'SELECT * FROM todos WHERE list_id = ?',
      [listId],
      { streams: [
          { name: 'list_todos',
            parameters: { list_id: listId },
            waitForStream: true 
          }
        ]
      }
    );
    ```
  </Tab>

  <Tab title="Kotlin Compose">
    The `composeSyncStream` extension subscribes to a stream as long as a composable is part of the composition. It returns `SyncStreamStatus?` for the subscription so you can check sync state.

    <Note>
      The `composeSyncStream` helper was added in Kotlin SDK v1.11.0.
    </Note>

    ```kotlin theme={null}
    @Composable
    fun TodoList(database: PowerSyncDatabase, listId: String) {
      val status = database.composeSyncStream(
        name = "list_todos",
        parameters = mapOf("list_id" to JsonParam.String(listId))
      )

      if (status?.subscription?.hasSynced != true) {
        LoadingSpinner()
        return
      }

      val todos = database.getAll("SELECT * FROM todos WHERE list_id = ?", listOf(listId))
      TodoItems(todos = todos)
    }
    ```

    You can pass `ttl` and `priority` for cache duration and [sync priority](/sync/advanced/prioritized-sync):

    ```kotlin theme={null}
    database.composeSyncStream(
      name = "list_todos",
      parameters = mapOf("list_id" to JsonParam.String(listId)),
      ttl = 1.hours,
      priority = StreamPriority(1)
    )
    ```
  </Tab>
</Tabs>

## Type-Safe Stream Wrappers

When you generate your client-side schema from the [PowerSync Dashboard](https://dashboard.powersync.com) or CLI, typed stream wrappers are generated alongside the schema for all SDKs. These catch typos in stream names and parameter names at compile time — mistakes that would otherwise cause silent data-missing bugs only detectable by inspecting sync status.

For example, without typed wrappers this fails silently — no error, but data won't sync:

```js theme={null}
// Wrong stream name, wrong parameter key — no compile error, data just won't sync
await db.syncStream('note', { project_id: 'abc' }).subscribe();
```

### The Generated Code

The schema generator produces typed wrappers at the bottom of your generated schema file. Given streams like:

```yaml theme={null}
streams:
  lists:
    query: SELECT * FROM lists WHERE owner_id = auth.user_id()
  todos:
    query: SELECT * FROM todos WHERE list_id = subscription.parameter('list')
```

The generated output (JavaScript/TypeScript) looks like:

```typescript theme={null}
import { column, Schema, Table, PowerSyncDatabase, SyncStream } from '@powersync/web';
// OR: import { ... } from '@powersync/react-native';
// OR (Tauri): import { ... } from '@powersync/common';

// ... table definitions ...

export const AppSchema = new Schema({ lists, todos });

export function typedStreams(db: PowerSyncDatabase) {
  return {
    lists(): SyncStream {
      return db.syncStream('lists', {});
    },
    todos(params: { list: string }): SyncStream {
      return db.syncStream('todos', params);
    }
  };
}
```

### Usage

Use the generated wrappers instead of calling `db.syncStream()` directly. Each method returns a `SyncStream`, so you can chain `.subscribe()`, `.subscribe({ ttl, priority })`, and all other methods covered on this page.

<Tabs>
  <Tab title="JavaScript/TypeScript">
    ```typescript theme={null}
    import { typedStreams } from './powersync/schema';

    // Stream without subscription parameters
    const sub = await typedStreams(db).lists().subscribe();

    // Stream with subscription parameters — names and types are enforced
    const sub = await typedStreams(db).todos({ list: 'list-id-abc' }).subscribe();

    // Works with framework hooks
    const stream = useSyncStream(typedStreams(db).todos({ list: listId }));
    ```
  </Tab>

  <Tab title="Dart">
    ```dart theme={null}
    // Stream without subscription parameters
    final sub = await TypedSyncStreams(db).lists().subscribe();

    // Stream with subscription parameters
    final sub = await TypedSyncStreams(db).todos(list: 'list-id-abc').subscribe();
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    // Stream without subscription parameters
    val sub = TypedSyncStreams(db).lists().subscribe()

    // Stream with subscription parameters
    val sub = TypedSyncStreams(db).todos(list = "list-id-abc").subscribe()
    ```
  </Tab>

  <Tab title="Swift">
    ```swift theme={null}
    // Stream without subscription parameters
    let sub = try await TypedSyncStreams(db).lists().subscribe()

    // Stream with subscription parameters
    let sub = try await TypedSyncStreams(db).todos(list: "list-id-abc").subscribe()
    ```
  </Tab>

  <Tab title=".NET">
    ```csharp theme={null}
    // Stream without subscription parameters
    var sub = await new TypedSyncStreams(db).Lists().Subscribe();

    // Stream with subscription parameters
    var sub = await new TypedSyncStreams(db).Todos(list: "list-id-abc").Subscribe();
    ```
  </Tab>
</Tabs>

<Note>
  Type-safe wrappers are only generated for streams that do **not** have `auto_subscribe: true`. Auto-subscribe streams start syncing automatically on connect and don't require explicit client subscriptions, so no wrapper is generated for them.
</Note>

## Checking Sync Status

You can check whether a subscription has synced and monitor download progress:

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

    // Check if this subscription has completed initial sync
    const status = db.currentStatus.forStream(sub);
    console.log(status?.subscription.hasSynced);  // true/false
    console.log(status?.progress);                // download progress
    ```
  </Tab>

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

    // Check if this subscription has completed initial sync
    final status = db.currentStatus.forStream(sub);
    print(status?.subscription.hasSynced);  // true/false
    print(status?.progress);                 // download progress
    ```
  </Tab>

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

    // Check if this subscription has completed initial sync
    val status = database.currentStatus.forStream(sub)
    println(status?.subscription?.hasSynced)  // true/false
    println(status?.progress)                  // download progress
    ```
  </Tab>

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

    // Check if this subscription has completed initial sync
    let status = db.currentStatus.forStream(stream: sub)
    print(status?.subscription.hasSynced ?? false)  // true/false
    print(status?.progress)                          // download progress
    ```
  </Tab>

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

    // Check if this subscription has completed initial sync
    var status = db.CurrentStatus.ForStream(sub);
    Console.WriteLine(status?.Subscription.HasSynced);  // true/false
    Console.WriteLine(status?.Progress);                 // download progress
    ```
  </Tab>
</Tabs>

## TTL (Time-To-Live)

TTL controls how long data remains cached after you unsubscribe. This enables "warm cache" behavior — when users navigate back to a screen, data may already be available without waiting for a sync.

**Default behavior:** Data is cached for 24 hours after unsubscribing. For most apps, this default works well.

### Setting a Custom TTL

<Tabs>
  <Tab title="TypeScript/JavaScript">
    ```js theme={null}
    // Cache for 1 hour after unsubscribe (TTL in seconds)
    const sub = await db.syncStream('todos', { list_id: 'abc' })
      .subscribe({ ttl: 3600 });

    // Cache indefinitely (data never expires)
    const sub = await db.syncStream('todos', { list_id: 'abc' })
      .subscribe({ ttl: Infinity });

    // No caching (remove data immediately on unsubscribe)
    const sub = await db.syncStream('todos', { list_id: 'abc' })
      .subscribe({ ttl: 0 });
    ```
  </Tab>

  <Tab title="Dart">
    ```dart theme={null}
    // Cache for 1 hour after unsubscribe
    final sub = await db.syncStream('todos', {'list_id': 'abc'})
      .subscribe(ttl: const Duration(hours: 1));

    // Cache for 7 days
    final sub = await db.syncStream('todos', {'list_id': 'abc'})
      .subscribe(ttl: const Duration(days: 7));
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    // Cache for 1 hour after unsubscribe
    val sub = database.syncStream("todos", mapOf("list_id" to JsonParam.String("abc")))
      .subscribe(ttl = 1.hours)

    // Cache for 7 days
    val sub = database.syncStream("todos", mapOf("list_id" to JsonParam.String("abc")))
      .subscribe(ttl = 7.days)
    ```
  </Tab>

  <Tab title="Swift">
    ```swift theme={null}
    // Cache for 1 hour after unsubscribe (TTL in seconds)
    let sub = try await db.syncStream(name: "todos", params: ["list_id": JsonValue.string("abc")])
      .subscribe(ttl: 60 * 60, priority: nil)

    // Cache for 7 days
    let sub = try await db.syncStream(name: "todos", params: ["list_id": JsonValue.string("abc")])
      .subscribe(ttl: 60 * 60 * 24 * 7, priority: nil)
    ```
  </Tab>

  <Tab title=".NET">
    ```csharp theme={null}
    // Cache for 1 hour after unsubscribe
    var sub = await db.SyncStream("todos", new() { ["list_id"] = "abc" })
      .Subscribe(new SyncStreamSubscribeOptions { Ttl = TimeSpan.FromHours(1) });

    // Cache for 7 days
    var sub = await db.SyncStream("todos", new() { ["list_id"] = "abc" })
      .Subscribe(new SyncStreamSubscribeOptions { Ttl = TimeSpan.FromDays(7) });
    ```
  </Tab>
</Tabs>

### How TTL Works

* **Per-subscription**: Each `(stream name, parameters)` pair has its own TTL.
* **First subscription wins**: If you subscribe to the same stream with the same parameters multiple times, the TTL from the first subscription is used.
* **After unsubscribe**: Data continues syncing for the TTL duration, then is removed from the client-side SQLite database.

```js theme={null}
// Example: User opens two lists with different TTLs
const subA = await db.syncStream('todos', { list_id: 'A' }).subscribe({ ttl: 43200 }); // 12h
const subB = await db.syncStream('todos', { list_id: 'B' }).subscribe({ ttl: 86400 }); // 24h

// Each subscription is independent
// List A data cached for 12h after unsubscribe
// List B data cached for 24h after unsubscribe
```

## Priority Override

Streams can have a default priority set in the YAML sync configuration (see [Prioritized Sync](/sync/advanced/prioritized-sync)). When subscribing, you can override this priority for a specific subscription:

```js theme={null}
// Override the stream's default priority
const sub = await db.syncStream('todos', { list_id: 'abc' }).subscribe({ priority: 1 });
```

When different components subscribe to the same stream with the same parameters but different priorities, PowerSync uses the highest priority for syncing. That higher priority is kept until the subscription ends (or its TTL expires). Subscriptions with different parameters are independent and do not conflict.

## Connection Parameters

Connection parameters are a more advanced feature for values that apply to all streams in a session. They're the Sync Streams equivalent of [Client Parameters](/sync/rules/client-parameters) in legacy Sync Rules.

<Note>
  For most use cases, **subscription parameters** (passed when subscribing) are more flexible and recommended. Use connection parameters only when you need a single global value across all streams, like an environment flag.
</Note>

Define streams that use connection parameters:

```yaml theme={null}
streams:
  config:
    auto_subscribe: true
    query: SELECT * FROM config WHERE env = connection.parameter('environment')
```

Set connection parameters when connecting:

<Note>
  **Tauri SDK**: Since `connect()` must be called from Rust for Tauri, connection parameters are passed via `SyncOptions` in your Rust connector. See the [Tauri SDK reference](/client-sdks/reference/tauri#2-instantiate-the-powersync-database) for details on setting up the Rust connector.
</Note>

<Tabs>
  <Tab title="TypeScript/JavaScript">
    ```js theme={null}
    await db.connect(connector, {
      params: { environment: 'production' }
    });
    ```
  </Tab>

  <Tab title="Dart">
    ```dart theme={null}
    await db.connect(
      connector: connector,
      params: {'environment': 'production'},
    );
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    database.connect(
      connector,
      params = mapOf("environment" to JsonParam.String("production"))
    )
    ```
  </Tab>

  <Tab title="Swift">
    ```swift theme={null}
    try await db.connect(
      connector: connector,
      options: ConnectOptions(params: ["environment": JsonValue.string("production")])
    )
    ```
  </Tab>

  <Tab title=".NET">
    ```csharp theme={null}
    await db.Connect(connector, new PowerSyncConnectionOptions {
      Params = new() { ["environment"] = "production" }
    });
    ```
  </Tab>
</Tabs>

## Opting Out of Auto-Subscribed Streams

By default, every stream defined with [`auto_subscribe: true`](/sync/streams/overview#using-auto-subscribe) starts syncing as soon as the client connects. For advanced use cases where the client needs more control over what syncs at connect time, you can set `includeDefaultStreams: false` on the connect options. The `auto_subscribe: true` flag is then ignored for that connection, and those streams only sync if the client explicitly calls `db.syncStream(name, params).subscribe()`.

Most apps will not need this option. Defining streams without `auto_subscribe: true` and subscribing on-demand from the client generally achieves a similar result with less indirection. One scenario where it can help is during a migration from auto-subscribed streams to on-demand subscriptions. You keep the auto-subscribed streams in place so older client versions continue to sync correctly, while newer versions that explicitly subscribe to the streams they need set `includeDefaultStreams: false` to skip those defaults.

<Tabs>
  <Tab title="TypeScript/JavaScript">
    ```js theme={null}
    // Connect without syncing auto_subscribe streams
    await db.connect(connector, {
      includeDefaultStreams: false
    });

    // Later, opt in to a specific stream
    const sub = await db.syncStream('my_stream', params).subscribe();
    ```
  </Tab>

  <Tab title="Dart">
    ```dart theme={null}
    await db.connect(
      connector: connector,
      options: const SyncOptions(includeDefaultStreams: false),
    );
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    database.connect(
      connector,
      options = SyncOptions(includeDefaultStreams = false)
    )
    ```
  </Tab>

  <Tab title="Swift">
    ```swift theme={null}
    try await db.connect(
      connector: connector,
      options: ConnectOptions(includeDefaultStreams: false)
    )
    ```
  </Tab>

  <Tab title=".NET">
    ```csharp theme={null}
    await db.Connect(connector, new PowerSyncConnectionOptions {
      IncludeDefaultStreams = false
    });
    ```
  </Tab>
</Tabs>

## API Reference

For quick reference, here are the key methods available in each SDK:

| Method                            | Description                                                                                                    |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `db.syncStream(name, params)`     | Get a `SyncStream` instance for a stream with optional parameters                                              |
| `stream.subscribe(options)`       | Subscribe to the stream. Returns a `SyncStreamSubscription`                                                    |
| `subscription.waitForFirstSync()` | Wait until the subscription has completed its initial sync                                                     |
| `subscription.unsubscribe()`      | Unsubscribe from the stream (data [remains cached](/sync/streams/client-usage#how-ttl-works) for TTL duration) |
| `db.currentStatus.forStream(sub)` | Get sync status and progress for a subscription                                                                |
