Skip to main content
After defining your 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). This page covers everything you need to use Sync Streams from your client code.

Quick Start

Streams that are configured to 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.
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 section below for how Tauri handles connect-time parameters differently.
// 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();

Framework Integrations

Most developers use framework-specific hooks that handle subscription lifecycle automatically.
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:
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.
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:
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} />;
}

Type-Safe Stream Wrappers

When you generate your client-side schema from the PowerSync Dashboard 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:
// 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:
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:
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.
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 }));
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.

Checking Sync Status

You can check whether a subscription has synced and monitor download progress:
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

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

// 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 });

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.
// 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). When subscribing, you can override this priority for a specific subscription:
// 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 in legacy Sync Rules.
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.
Define streams that use connection parameters:
streams:
  config:
    auto_subscribe: true
    query: SELECT * FROM config WHERE env = connection.parameter('environment')
Set connection parameters when connecting:
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 for details on setting up the Rust connector.
await db.connect(connector, {
  params: { environment: 'production' }
});

API Reference

For quick reference, here are the key methods available in each SDK:
MethodDescription
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 for TTL duration)
db.currentStatus.forStream(sub)Get sync status and progress for a subscription