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

# PowerSync Setup Guide

> Step-by-step guide to adding PowerSync to your app, from connecting your source database to integrating the client SDK.

# 1. Configure Your Source Database

PowerSync needs to connect to your source database (Postgres, MongoDB, MySQL or SQL Server) to replicate data. Before setting up PowerSync, you need to configure your database with the appropriate permissions and replication settings.

<Tip>
  Using the [PowerSync CLI](/tools/cli) and want an automatically integrated Postgres instance for local development? You can skip to [Step 2](#2-set-up-powersync-service-instance) and set one up with the **CLI (Self-Hosted)** tab.
</Tip>

<Tabs>
  <Tab title="Postgres">
    Configuring Postgres for PowerSync involves three main tasks:

    1. **Enable logical replication**: PowerSync reads the Postgres WAL using logical replication. Set `wal_level = logical` in your Postgres configuration.
    2. **Create a PowerSync database user**: Create a role with replication privileges and read-only access to your tables.
    3. **Create a `powersync` publication**: Create a logical replication publication named `powersync` to specify which tables to replicate.

    <CodeGroup>
      ```sql General theme={null}
      -- 1. Enable logical replication (requires restart)
      ALTER SYSTEM SET wal_level = logical;

      -- 2. Create PowerSync database user/role with replication privileges and read-only access to your tables
      CREATE ROLE powersync_role WITH REPLICATION BYPASSRLS LOGIN PASSWORD 'myhighlyrandompassword';

      -- Set up permissions for the newly created role
      -- Read-only (SELECT) access is required
      GRANT SELECT ON ALL TABLES IN SCHEMA public TO powersync_role;

      -- Optionally, grant SELECT on all future tables (to cater for schema additions)
      ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO powersync_role;

      -- 3. Create a publication to replicate tables. The publication must be named "powersync"
      CREATE PUBLICATION powersync FOR ALL TABLES;
      ```

      ```sql Supabase theme={null}
      -- Supabase has logical replication enabled by default
      -- Just create the user and publication:

      -- Create PowerSync database user/role with replication privileges and read-only access to your tables
      CREATE ROLE powersync_role WITH REPLICATION BYPASSRLS LOGIN PASSWORD 'myhighlyrandompassword';

      -- Set up permissions for the newly created role
      -- Read-only (SELECT) access is required
      GRANT SELECT ON ALL TABLES IN SCHEMA public TO powersync_role;

      -- Optionally, grant SELECT on all future tables (to cater for schema additions)
      ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO powersync_role;

      -- Create a publication to replicate tables. The publication must be named "powersync"
      CREATE PUBLICATION powersync FOR ALL TABLES;
      ```

      ```bash Docker (Self-hosting) theme={null}
      # 1. Create a Docker network (if not already created)
      # This allows various PowerSync containers to communicate with each other
      docker network create powersync-network

      # 2. Run Postgres source database with logical replication enabled (required for PowerSync)
      docker run -d \
        --name powersync-postgres \
        --network powersync-network \
        -e POSTGRES_PASSWORD="my_secure_password" \
        -p 5432:5432 \
        postgres:18 \
        postgres -c wal_level=logical

      # 3. Configure PowerSync user and publication
      # This creates a PowerSync database user/role with replication privileges and read-only access to your tables
      # Read-only (SELECT) access is also granted to all future tables (to cater for schema additions)
      # It also creates a publication to replicate tables. The publication must be named "powersync"
      docker exec -it powersync-postgres psql -U postgres -c "
      CREATE ROLE powersync_role WITH REPLICATION BYPASSRLS LOGIN PASSWORD 'myhighlyrandompassword';
      GRANT SELECT ON ALL TABLES IN SCHEMA public TO powersync_role;
      ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO powersync_role;
      CREATE PUBLICATION powersync FOR ALL TABLES;"
      ```
    </CodeGroup>

    <Note>
      * **Version compatibility**: PowerSync requires Postgres version 11 or greater.
    </Note>

    <Tip>
      **Learn More**

      * For more details on Postgres setup, including provider-specific guides (Supabase, AWS RDS, etc.), see [Source Database Setup](/configuration/source-db/setup#postgres).
      * **Self-hosting PowerSync?** See the [Self-Host-Demo App](https://github.com/powersync-ja/self-host-demo/tree/main/demos/nodejs) for a complete working example of connecting a Postgres source database to PowerSync.
    </Tip>
  </Tab>

  <Tab title="MongoDB Atlas">
    For MongoDB Atlas databases, the minimum permissions when using built-in roles are:

    ```
    read@<your_database>
    readWrite@<your_database>._powersync_checkpoints
    ```

    To allow PowerSync to automatically enable `changeStreamPreAndPostImages` on replicated collections (optional, but recommended), additionally add:

    ```
    dbAdmin@<your_database>
    ```

    <Note>
      **Version compatibility**: PowerSync requires MongoDB version 6.0 or greater.
    </Note>

    <Tip>
      **Learn More**

      * For more details including instructions for self-hosted MongoDB, or for custom roles on MongoDB Atlas, see [Source Database Setup](/configuration/source-db/setup#mongodb).
      * **Self-hosting PowerSync?** See the [Self-Host-Demo App](https://github.com/powersync-ja/self-host-demo/tree/main/demos/nodejs-mongodb) for a complete working example of connecting a MongoDB source database to PowerSync.
    </Tip>
  </Tab>

  <Tab title="MySQL (beta)">
    For MySQL, you need to configure binary logging and create a user with replication privileges:

    ```sql theme={null}
      -- Configure binary logging
      -- Add to MySQL option file (my.cnf or my.ini):
      server_id=<Unique Integer Value>
      log_bin=ON
      enforce_gtid_consistency=ON
      gtid_mode=ON
      binlog_format=ROW

      -- Create a user with necessary privileges
      CREATE USER 'repl_user'@'%' IDENTIFIED BY '<password>';

      -- Grant replication client privilege
      GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl_user'@'%';

      -- Grant select access to the specific database
      GRANT SELECT ON <source_database>.* TO 'repl_user'@'%';

      -- Apply changes
      FLUSH PRIVILEGES;
    ```

    <Note>
      **Version compatibility**: PowerSync requires MySQL version 5.7 or greater.
    </Note>

    <Tip>
      **Learn More**

      * For more details on MySQL setup, see [Source Database Setup](/configuration/source-db/setup#mysql).
      * **Self-hosting PowerSync?** See the [Self-Host-Demo App](https://github.com/powersync-ja/self-host-demo/tree/main/demos/nodejs-mysql) for a complete working example of connecting a MySQL source database to PowerSync.
    </Tip>
  </Tab>

  <Tab title="SQL Server (Beta)">
    Refer to [these instructions](/configuration/source-db/setup#sql-server).

    **Self-hosting PowerSync?** See the [Self-Host-Demo App](https://github.com/powersync-ja/self-host-demo/tree/main/demos/nodejs-mssql) for a complete working example of connecting a SQL Server source database to PowerSync.
  </Tab>
</Tabs>

# 2. Set Up PowerSync Service Instance

PowerSync is available as a cloud-hosted service (PowerSync Cloud) or can be self-hosted (PowerSync Open Edition or PowerSync Enterprise Self-Hosted Edition).

<Tabs>
  <Tab title="Dashboard (Cloud)">
    If you haven't yet, sign up for a free PowerSync Cloud account [here](https://accounts.powersync.com/portal/powersync-signup?s=docs).

    After signing up, you will be taken to the [PowerSync Dashboard](https://dashboard.powersync.com/).

    Here, create a new project. *Development* and *Production* instances of the PowerSync Service will be created by default in the project.
  </Tab>

  <Tab title="CLI (Cloud)">
    If you haven't yet, sign up for a free PowerSync Cloud account [here](https://accounts.powersync.com/portal/powersync-signup?s=docs).

    Install the [PowerSync CLI](/tools/cli) (requires Node.js/npm), then log in and scaffold the config directory:

    ```bash theme={null}
    npm install -g powersync
    powersync login
    powersync init cloud
    ```

    This creates a `powersync/` directory with `service.yaml` (instance name, region, connection, auth) and `sync-config.yaml` (sync config). Edit `powersync/service.yaml` to set your instance name and region — you'll configure the database connection in the next step.

    Then create the Cloud instance:

    ```bash theme={null}
    powersync link cloud --create --project-id=<project-id>
    ```

    Find your project ID in the [PowerSync Dashboard](https://dashboard.powersync.com) URL, or run `powersync fetch instances` after logging in.
  </Tab>

  <Tab title="CLI (Self-Hosted)">
    Recommended for getting started: the CLI scaffolds your config directory and generates the Docker Compose stack (including a Postgres instance for the source database and storage) so you can run PowerSync locally with minimal setup. For custom setups use the **Manual (Self-Hosted)** tab. Install the [PowerSync CLI](/tools/cli) (requires Node.js/npm); alternative installation options (e.g. installers via GitHub releases) will be available in the near future. Then run:

    ```bash theme={null}
    npm install -g powersync
    powersync init self-hosted
    powersync docker configure --database postgres --storage postgres
    ```

    Docker sets up Postgres for both the source database and bucket storage and creates `powersync/docker/docker-compose.yaml`. Other databases are supported as well, you will learn more about this in the next step. Before starting, replace `powersync/sync-config.yaml` with this minimal sync config:

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

    streams:
      todos:
        # Streams without parameters sync the same data to all users
        auto_subscribe: true
        query: "SELECT * FROM todos"
    ```

    You'll update this with your actual tables/collections in a later step.

    The Docker Postgres instance runs init scripts only on first start. Create your specific tables before running `powersync docker start` for the first time. See the [Docker usage docs](https://github.com/powersync-ja/powersync-cli/blob/main/docs/usage-docker.md) in the PowerSync CLI repository for more details.

    Then start the PowerSync Service:

    ```bash theme={null}
    powersync docker start
    ```

    Run `powersync status` to verify it's running.

    <Tip>
      **Learn More**

      * [Self-Hosting Introduction](/intro/self-hosting)
      * [Self-Host Demo App](https://github.com/powersync-ja/self-host-demo) for complete working examples.
      * [Self-Hosted Service Configuration](/configuration/powersync-service/self-hosted-instances) for more details on the config file structure.
      * [CLI documentation](/tools/cli)
    </Tip>
  </Tab>

  <Tab title="Manual (Self-Hosted)">
    Self-hosted PowerSync runs via Docker. The commands below illustrate the basic PowerSync Service requirements.

    Below is a minimal example using Postgres for bucket storage. MongoDB is also supported as bucket storage. The source database connection is configured in the next step — you can use the Docker-managed Postgres from Step 1 or point to an external database instead.

    ```bash theme={null}
    # 1. Create a directory for your config
    mkdir powersync-service && cd powersync-service

    # 2. Set up bucket storage (Postgres and MongoDB are supported)
    docker run -d \
      --name powersync-postgres-storage \
      --network powersync-network \
      -p 5433:5432 \
      -e POSTGRES_PASSWORD="my_secure_storage_password" \
      -e POSTGRES_DB=powersync_storage \
      postgres:18

    ## Set up Postgres storage user
    docker exec -it powersync-postgres-storage psql -U postgres -d powersync_storage -c "
    CREATE USER powersync_storage_user WITH PASSWORD 'my_secure_user_password';
    GRANT CREATE ON DATABASE powersync_storage TO powersync_storage_user;"

    # 3. Create service.yaml (see below)

    # 4. Run PowerSync Service
    # The Service config can be specified as an environment variable (shown below), as a filepath, or as a command line parameter
    # See these docs for more details: https://docs.powersync.com/configuration/powersync-service/self-hosted-instances
    docker run -d \
      --name powersync \
      --network powersync-network \
      -p 8080:8080 \
      -e POWERSYNC_CONFIG_B64="$(base64 -i ./service.yaml)" \
      journeyapps/powersync-service:latest
    ```

    **Basic `service.yaml` structure:**

    ```yaml theme={null}
    # Source database connection (see the next step for more details)
    replication:
      connections:
        - type: postgresql # or mongodb, mysql, mssql
          uri: postgresql://powersync_role:myhighlyrandompassword@powersync-postgres:5432/postgres
          sslmode: disable  # Only for local/private networks

    # Connection settings for bucket storage (Postgres and MongoDB are supported)
    storage:
      type: postgresql
      uri: postgresql://powersync_storage_user:my_secure_user_password@powersync-postgres-storage:5432/powersync_storage
      sslmode: disable  # Use 'disable' only for local/private networks

    # Sync Streams config (defined in a later step)
    sync_config:
      content: |
        config:
          edition: 3
        streams:
          shared_data:
            auto_subscribe: true
            queries:
              - SELECT * FROM lists
              - SELECT * FROM todos
    ```

    <Warning>
      **Note**: This example assumes you've configured your source database with the required user and publication (see the previous step)
      and are running it via Docker in the 'powersync-network' network.

      If you are not using Docker, you will need to specify the connection details in the `service.yaml` file manually (see next step for more details).
    </Warning>

    <Tip>
      **Learn More**

      * [Self-Hosting Introduction](/intro/self-hosting)
      * [Self-Host Demo App](https://github.com/powersync-ja/self-host-demo) for complete working examples.
      * [Self-Hosted Service Configuration](/configuration/powersync-service/self-hosted-instances) for more details on the config file structure.
      * [CLI documentation](/tools/cli)
    </Tip>
  </Tab>
</Tabs>

# 3. Connect PowerSync to Your Source Database

The next step is to connect your PowerSync Service instance to your source database.

<Tabs>
  <Tab title="Dashboard (Cloud)">
    In the [PowerSync Dashboard](https://dashboard.powersync.com/), select your project and instance, then go to **Database Connections**:

    1. Click **Connect to Source Database**
    2. Select the appropriate database type tab (Postgres, MongoDB, MySQL or SQL Server)
    3. Fill in your connection details:
           <Note>
             **Note**: Use the username (e.g., `powersync_role`) and password you created in Step 1: Configure your Source Database.
           </Note>
       * **Postgres**: Host, Port (5432), Database name, Username, Password, SSL Mode
       * **MongoDB**: Connection URI (e.g., `mongodb+srv://user:pass@cluster.mongodb.net/database`)
       * **MySQL**: Host, Port (3306), Database name, Username, Password
       * **SQL Server**: Name, Host, Port (1433), Database name, Username, Password
    4. Click **Test Connection** to verify
    5. Click **Save Connection**

    PowerSync will now deploy and configure an isolated cloud environment, which can take a few minutes.

    <Tip>
      **Learn More**

      For more details on database connections, including provider-specific connection details (Supabase, AWS RDS, MongoDB Atlas, etc.), see [Source Database Connection](/configuration/source-db/connection).
    </Tip>
  </Tab>

  <Tab title="CLI (Cloud)">
    Edit `powersync/service.yaml` (created in the previous step) with your connection details. Use `!env` for secrets:

    <Note>
      **Note**: Use the username (e.g., `powersync_role`) and password you created in Step 1: Configure your Source Database.
    </Note>

    <CodeGroup>
      ```yaml Postgres theme={null}
      replication:
        connections:
          - type: postgresql
            uri: postgresql://powersync_role:myhighlyrandompassword@host:5432/postgres
            sslmode: disable # 'verify-full' (default) or 'verify-ca' or 'disable'
            # Note: 'disable' is only suitable for local/private networks
      ```

      ```yaml MongoDB theme={null}
      replication:
        connections:
          - type: mongodb
            uri: mongodb+srv://user:password@cluster.mongodb.net/database
            post_images: auto_configure
      ```

      ```yaml MySQL theme={null}
      replication:
        connections:
          - type: mysql
            uri: mysql://repl_user:password@host:3306/database
      ```

      ```yaml SQL Server theme={null}
      replication:
        connections:
          - type: mssql
            uri: mssql://user:password@host:1433/database
            schema: dbo
      ```
    </CodeGroup>

    You will run `powersync deploy` in a later step to deploy your config to the PowerSync Cloud instance.

    <Tip>
      **Learn More**

      For more details on database connections, including provider-specific connection details (Supabase, AWS RDS, MongoDB Atlas, etc.), see [Source Database Connection](/configuration/source-db/connection).
    </Tip>
  </Tab>

  <Tab title="CLI (Self-Hosted)">
    If you used Docker in the previous step, the source database connection is already configured. `service.yaml` reads the connection URI from `!env PS_DATA_SOURCE_URI`. The Docker-managed Postgres (`pg-db`) was also pre-configured with `wal_level=logical` and a `powersync` publication by the init scripts.

    If you want to use an **external database** instead, update `PS_DATA_SOURCE_URI` in `powersync/docker/.env` with your connection details, then restart:

    ```bash theme={null}
    powersync docker reset
    ```

    You'll also need to complete the source database setup from Step 1 (replication user, publication) on your external database before this will work.
  </Tab>

  <Tab title="Manual (Self-Hosted)">
    Configure the source database connection in your `service.yaml` file (as you did in the previous step). Examples for the different database types are below.

    <Note>
      **Note**: Use the username (e.g., `powersync_role`) and password you created in Step 1: Configure your Source Database.
    </Note>

    <CodeGroup>
      ```yaml Postgres theme={null}
      replication:
        connections:
          - type: postgresql
            uri: postgresql://powersync_role:myhighlyrandompassword@powersync-postgres:5432/postgres
            sslmode: disable # 'verify-full' (default) or 'verify-ca' or 'disable'
            # Note: 'disable' is only suitable for local/private networks, not for public networks
      ```

      ```yaml MySQL theme={null}
      replication:
        connections:
          - type: mysql
            uri: mysql://repl_user:password@host:3306/database
      ```

      ```yaml SQL Server theme={null}
      replication:
        connections:
          - type: mssql
            uri: mssql://user:password@$host:1433/database
            schema: dbo
            additionalConfig:
              trustServerCertificate: true
              pollingIntervalMs: 1000
              pollingBatchSize: 20
      ```
    </CodeGroup>

    <Tip>
      **Learn More**

      See the [self-host-demo app](https://github.com/powersync-ja/self-host-demo) for complete working examples of the different database types.
    </Tip>
  </Tab>
</Tabs>

# 4. Define Sync Streams

PowerSync uses **Sync Streams** to control which data gets synced to which users/devices, using SQL-like queries defined in YAML format.

Start with simple **auto-subscribed streams** that sync data to all users by default:

<CodeGroup>
  ```yaml Postgres Example theme={null}
  config:
    edition: 3
  streams:
    shared_data:
      auto_subscribe: true
      queries:
        - SELECT * FROM todos
        - SELECT * FROM lists WHERE NOT archived
  ```

  ```yaml MongoDB Example theme={null}
  config:
    edition: 3
  streams:
    shared_data:
      auto_subscribe: true
      # MongoDB uses "_id" but PowerSync uses "id" on the client
      queries:
        - SELECT _id as id, * FROM lists
        - SELECT _id as id, * FROM todos WHERE archived = false
  ```

  ```yaml MySQL Example theme={null}
  config:
    edition: 3
  streams:
    shared_data:
      auto_subscribe: true
      queries:
        - SELECT * FROM todos
        - SELECT * FROM lists WHERE NOT archived
  ```

  ```yaml SQL Server Example theme={null}
  config:
    edition: 3
  streams:
    shared_data:
      auto_subscribe: true
      queries:
        - SELECT * FROM todos
        - SELECT * FROM lists WHERE NOT archived
  ```
</CodeGroup>

**Learn more:** [Sync Streams documentation](/sync/streams/overview)

### Deploy Your Configuration

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

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

  <Tab title="CLI (Cloud)">
    Edit `powersync/sync-config.yaml` with your sync config, then validate and deploy to the linked Cloud instance:

    ```bash theme={null}
    powersync validate
    powersync deploy
    ```

    This deploys your full config (connection, auth, and sync config). For subsequent sync-only changes, use `powersync deploy sync-config` instead.
  </Tab>

  <Tab title="CLI (Self-Hosted)">
    Edit `powersync/sync-config.yaml` with your sync config. The default file has a placeholder (`SELECT * FROM todos`) — replace it with your actual table/collection names. Then apply the changes:

    ```bash theme={null}
    powersync validate
    powersync docker reset
    ```
  </Tab>

  <Tab title="Manual (Self-Hosted)">
    Add a `sync_config` section to your `service.yaml`. Using a separate file (recommended) keeps the main config tidy:

    **Recommended — reference a separate file:**

    ```yaml service.yaml theme={null}
    sync_config:
      path: sync-config.yaml
    ```

    Put your streams in `sync-config.yaml` (see [Self-Hosted Instance Configuration](/configuration/powersync-service/self-hosted-instances#sync-streams) for full examples). Alternatively, you can use inline `content: |` with the YAML nested under `sync_config`.
  </Tab>
</Tabs>

<Note>
  Table/collection names in your configuration must match the table names defined in your client-side schema (defined in a later step below).
</Note>

# 5. Generate a Development Token

For quick development and testing, you can generate a temporary development token instead of implementing full authentication.

You'll use this token for two purposes:

* **Testing with the *Sync Diagnostics Client*** (in the next step) to verify your setup and Sync Streams
* **Connecting your app** (in a later step) to test the client SDK integration

<Tabs>
  <Tab title="Dashboard (Cloud)">
    1. In the [PowerSync Dashboard](https://dashboard.powersync.com/), select your project and instance
    2. Go to the **Client Auth** view
    3. Check the **Development tokens** setting and save your changes
    4. Click the **Connect** button in the top bar
    5. **Enter token subject**: Since you're starting with simple streams or buckets that sync all data to all users (as we recommended in the previous step), you can just put something like `test-user` as the token subject (which would normally be the user ID you want to test with).
    6. Click **Generate token** and copy the token

    <Note>
      Development tokens expire after 12 hours.
    </Note>
  </Tab>

  <Tab title="CLI (Cloud)">
    Generate a development token with:

    ```bash theme={null}
    powersync generate token --subject=test-user
    ```

    Replace `test-user` with a user ID of your choice (this would normally be the user ID you want to test with).

    Requires `allow_temporary_tokens` to be enabled on the instance. Add it to `powersync/service.yaml` if you haven't already, then redeploy:

    ```yaml theme={null}
    client_auth:
      allow_temporary_tokens: true
    ```

    ```bash theme={null}
    powersync deploy
    ```

    <Note>
      Development tokens expire after 12 hours.
    </Note>
  </Tab>

  <Tab title="Self-Hosted">
    Follow the steps below. Steps 1 and 2 configure signing keys and your PowerSync config; in Step 3 you can use the **CLI (recommended)** or the test-client to generate the token.

    <Steps>
      <Step title="Step 1: Generate signing keys">
        Generate a temporary private/public key-pair (RS256) or shared key (HS256) for JWT signing and verification.

        <Tabs>
          <Tab title="RS256">
            Use an online JWK generator like [mkjwk.org](https://mkjwk.org/) (select RSA, 2048 bits, Signature use, RS256 algorithm).

            Or generate locally with Node.js:

            ```bash theme={null}
            # Install pem-jwk if needed
            npm install -g pem-jwk

            # Generate private key
            openssl genrsa -out private-key.pem 2048

            # Convert public key to JWK format
            openssl rsa -in private-key.pem -pubout | pem-jwk
            ```
          </Tab>

          <Tab title="HS256">
            Use an online JWK generator like [mkjwk.org](https://mkjwk.org/) (select oct, 256 bits, Signature use, HS256 algorithm) - this outputs base64url directly.

            Or generate and convert using OpenSSL:

            ```bash theme={null}
            # Generate and convert to base64url
            openssl rand -base64 32 | tr '+/' '-_' | tr -d '='
            ```

            <Warning>
              For production environments, shared secrets (HS256) are not recommended.
            </Warning>
          </Tab>
        </Tabs>
      </Step>

      <Step title="Step 2: Update your config">
        Add the `client_auth` parameter to your PowerSync config (e.g. `service.yaml`):

        <Tabs>
          <Tab title="RS256">
            Copy the JWK values from [mkjwk.org](https://mkjwk.org/) or the `pem-jwk` output, then add to your config:

            ```yaml service.yaml theme={null}
            # Client (application end user) authentication settings
            client_auth:
              # static collection of public keys for JWT verification
              jwks:
                keys:
                  - kty: 'RSA'
                    n: '[rsa-modulus]'
                    e: '[rsa-exponent]'
                    alg: 'RS256'
                    kid: 'dev-key-1'
            ```
          </Tab>

          <Tab title="HS256">
            Copy the `k` value from mkjwk.org or the OpenSSL output, then add to your config:

            ```yaml service.yaml theme={null}
            # Client (application end user) authentication settings
            client_auth:
              audience: ['http://localhost:8080', 'http://127.0.0.1:8080']
              # static collection of public keys for JWT verification
              jwks:
                keys:
                  - kty: oct
                    alg: 'HS256'
                    k: '[base64url-encoded-shared-secret]'
                    kid: 'dev-key-1'
            ```
          </Tab>
        </Tabs>

        <Note>
          These examples use static `jwks: keys:` for simplicity. For production, we recommend using `jwks_uri` to point to a JWKS endpoint instead. See [Custom Authentication](/configuration/auth/custom) for more details.
        </Note>
      </Step>

      <Step title="Step 3: Generate a development token">
        Choose either the [PowerSync CLI](/tools/cli) (recommended) or the test-client:

        <Tabs>
          <Tab title="CLI (recommended)">
            Apply your config changes (e.g. restart your PowerSync Service or run `powersync docker reset` if running locally with Docker), then run:

            ```bash theme={null}
            powersync generate token --subject=test-user
            ```

            Replace `test-user` with the user ID you want to authenticate:

            * If your Sync Streams aren't filtered by user (same data syncs to all users), you can use any value (e.g., `test-user`).
            * If your data is filtered by <Tooltip tip="Values such as user ID that are used to determine which data syncs to which user.">parameters</Tooltip>, use a user ID that matches a user in your database. PowerSync uses this value (e.g. via `auth.user_id()`) to determine what to sync.
          </Tab>

          <Tab title="test-client">
            1. If you have not done so already, clone the [`powersync-service` repo](https://github.com/powersync-ja/powersync-service/tree/main)
            2. Install and build:

            * In the project root: `pnpm install` and `pnpm build`
            * In the `test-client` directory: `pnpm build`

            3. Generate a token from the `test-client` directory, pointing at your config file:

            ```bash theme={null}
            node dist/bin.js generate-token --config path/to/service.yaml --sub test-user
            ```

            Replace `test-user` with the user ID you want to authenticate:

            * If your Sync Streams aren't filtered by user (same data syncs to all users), you can use any value (e.g., `test-user`).
            * If your data is filtered by <Tooltip tip="Values such as user ID that are used to determine which data syncs to which user.">parameters</Tooltip>, use a user ID that matches a user in your database. PowerSync uses this value (e.g. via `auth.user_id()`) to determine what to sync.
          </Tab>
        </Tabs>
      </Step>
    </Steps>

    <Note>
      Development tokens expire after 12 hours.
    </Note>
  </Tab>
</Tabs>

# 6. \[Optional] Test Sync with the Sync Diagnostics Client

Before implementing the PowerSync Client SDK in your app, you can validate that syncing is working correctly using our [Sync Diagnostics Client](https://diagnostics-app.powersync.com) (this hosted version works with both PowerSync Cloud and self-hosted setups).

Use the development token you generated in the [previous step](#5-generate-a-development-token) to connect and verify your setup:

<Tabs>
  <Tab title="PowerSync Cloud">
    1. Go to [https://diagnostics-app.powersync.com](https://diagnostics-app.powersync.com)
    2. Enter your development token at **PowerSync Token** (from the [Generate a Development Token](#5-generate-a-development-token) step above)
    3. Enter your PowerSync instance URL at **PowerSync Endpoint** (found in the [PowerSync Dashboard](https://dashboard.powersync.com/) - click **Connect** in the top bar)
    4. Click **Proceed**
  </Tab>

  <Tab title="Self-Hosted">
    1. Go to [https://diagnostics-app.powersync.com](https://diagnostics-app.powersync.com)
    2. Enter your development token at **PowerSync Token** (from the [Generate a Development Token](#5-generate-a-development-token) step above)
    3. Enter your PowerSync Service endpoint at **PowerSync Endpoint** (the URL where your self-hosted service is running, e.g. `http://localhost:8080` if running locally)
    4. Click **Proceed**

    <Note>
      The Sync Diagnostics Client can also be run as a local standalone web app — see the [README](https://github.com/powersync-ja/powersync-js/tree/main/tools/diagnostics-app#readme) for instructions.
    </Note>
  </Tab>
</Tabs>

The Sync Diagnostics Client will connect to your PowerSync Service instance and display [information](https://github.com/powersync-ja/powersync-js/tree/main/tools/diagnostics-app#functionality) about the synced data, and allow you to [query](https://github.com/powersync-ja/powersync-js/tree/main/tools/diagnostics-app#sql-console) the client-side SQLite database.

<Check>
  **Checkpoint:**

  Inspect your synced tables in the Sync Diagnostics Client — these should match the Sync Streams you [defined previously](#4-define-sync-streams). This confirms your setup is working correctly before integrating the client SDK into your app.
</Check>

# 7. Use the Client SDK

Now it's time to integrate PowerSync into your app. This involves installing the Client SDK, defining your client-side schema, instantiating the database, connecting to your PowerSync Service instance, and reading/writing data.

### Install the Client SDK

Add the PowerSync Client SDK to your app project. PowerSync provides SDKs for various platforms and frameworks.

<Tabs>
  <Tab title="React Native / Expo">
    Add the [PowerSync React Native NPM package](https://www.npmjs.com/package/@powersync/react-native) to your project:

    <Tabs>
      <Tab title="npm">
        ```bash theme={null}
        npx expo install @powersync/react-native
        ```
      </Tab>

      <Tab title="yarn">
        ```bash theme={null}
        yarn expo add @powersync/react-native
        ```
      </Tab>

      <Tab title="pnpm">
        ```
        pnpm expo install @powersync/react-native
        ```
      </Tab>
    </Tabs>

    **Install Peer Dependencies**

    PowerSync requires a SQLite database adapter. Choose between:

    <Tabs>
      <Tab title="OP-SQLite (Recommended)">
        [PowerSync OP-SQLite](https://www.npmjs.com/package/@powersync/op-sqlite) offers:

        * Built-in encryption support via SQLCipher
        * Smoother transition to React Native's New Architecture

        <Tabs>
          <Tab title="npm">
            ```bash theme={null}
            npx expo install @powersync/op-sqlite @op-engineering/op-sqlite
            ```
          </Tab>

          <Tab title="yarn">
            ```bash theme={null}
            yarn expo add @powersync/op-sqlite @op-engineering/op-sqlite
            ```
          </Tab>

          <Tab title="pnpm">
            ```
            pnpm expo install @powersync/op-sqlite @op-engineering/op-sqlite
            ```
          </Tab>
        </Tabs>
      </Tab>

      <Tab title="React Native Quick SQLite">
        The [@journeyapps/react-native-quick-sqlite](https://www.npmjs.com/package/@journeyapps/react-native-quick-sqlite) package is the original database adapter for React Native and therefore more battle-tested in production environments.

        <Tabs>
          <Tab title="npm">
            ```bash theme={null}
            npx expo install @journeyapps/react-native-quick-sqlite
            ```
          </Tab>

          <Tab title="yarn">
            ```bash theme={null}
            yarn expo add @journeyapps/react-native-quick-sqlite
            ```
          </Tab>

          <Tab title="pnpm">
            ```
            pnpm expo install @journeyapps/react-native-quick-sqlite
            ```
          </Tab>
        </Tabs>

        **iOS with `use_frameworks!`**

        If your iOS project uses `use_frameworks!`, add the `react-native-quick-sqlite` plugin to your app.json or app.config.js and configure the staticLibrary option:

        ```
        {
          "expo": {
            "plugins": [
              [
                "@journeyapps/react-native-quick-sqlite",
                {
                  "staticLibrary": true
                }
              ]
            ]
          }
        }
        ```

        This plugin automatically configures the necessary build settings for `react-native-quick-sqlite` to work with `use_frameworks!`.
      </Tab>
    </Tabs>

    <Tip>
      **Using Expo Go?** Our native database adapters listed below (OP-SQLite and React Native Quick SQLite) are not compatible with Expo Go's sandbox environment. To run PowerSync with Expo Go install our JavaScript-based adapter `@powersync/adapter-sql-js` instead. See details [here](/client-sdks/frameworks/expo-go-support).
    </Tip>

    <Note>
      **Polyfills and additional notes:**

      * For async iterator support with watched queries, additional polyfills are required. See the [Babel plugins section](https://www.npmjs.com/package/@powersync/react-native#babel-plugins-watched-queries) in the README.
      * When using the **OP-SQLite** package, we recommend adding this [metro config](https://github.com/powersync-ja/powersync-js/tree/main/packages/react-native#metro-config-optional)
        to avoid build issues.
    </Note>
  </Tab>

  <Tab title="JavaScript Web">
    Add the [PowerSync Web NPM package](https://www.npmjs.com/package/@powersync/web) to your project:

    <Tabs>
      <Tab title="npm">
        ```bash theme={null}
        npm install @powersync/web
        ```
      </Tab>

      <Tab title="yarn">
        ```bash theme={null}
        yarn add @powersync/web
        ```
      </Tab>

      <Tab title="pnpm">
        ```bash theme={null}
        pnpm install @powersync/web
        ```
      </Tab>
    </Tabs>

    **Install Peer Dependencies**

    This SDK currently requires [`@journeyapps/wa-sqlite`](https://www.npmjs.com/package/@journeyapps/wa-sqlite) as a peer dependency. Install it in your app with:

    <Tabs>
      <Tab title="npm">
        ```bash theme={null}
        npm install @journeyapps/wa-sqlite
        ```
      </Tab>

      <Tab title="yarn">
        ```bash theme={null}
        yarn add @journeyapps/wa-sqlite
        ```
      </Tab>

      <Tab title="pnpm">
        ```bash theme={null}
        pnpm install @journeyapps/wa-sqlite
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab title="Node.js">
    Add the [PowerSync Node NPM package](https://www.npmjs.com/package/@powersync/node) to your project:

    <Tabs>
      <Tab title="npm">
        ```bash theme={null}
        npm install @powersync/node
        ```
      </Tab>

      <Tab title="yarn">
        ```bash theme={null}
        yarn add @powersync/node
        ```
      </Tab>

      <Tab title="pnpm">
        ```bash theme={null}
        pnpm install @powersync/node
        ```
      </Tab>
    </Tabs>

    **Install Peer Dependencies**

    The PowerSync SDK for Node.js supports multiple drivers. More details are available under [Encryption and Custom SQLite Drivers](/client-sdks/reference/node#encryption-and-custom-sqlite-drivers). We currently recommend the `better-sqlite3` package for most users:

    <Tabs>
      <Tab title="npm">
        ```bash theme={null}
        npm install better-sqlite3
        ```
      </Tab>

      <Tab title="yarn">
        ```bash theme={null}
        yarn add better-sqlite3
        ```
      </Tab>

      <Tab title="pnpm">
        ```bash theme={null}
        pnpm install better-sqlite3
        ```
      </Tab>
    </Tabs>

    <Warning>
      Previous versions of the PowerSync SDK for Node.js used the `@powersync/better-sqlite3` fork as a
      required peer dependency.
      This is no longer recommended. After upgrading to `@powersync/node` version `0.12.0` or later, ensure
      the old package is no longer installed by running `npm uninstall @powersync/better-sqlite3`
    </Warning>

    **Common Installation Issues**

    The `better-sqlite3` package requires native compilation, which depends on certain system tools.
    Prebuilt assets are available and used by default, but a custom compilation may be started depending on the Node.js
    or Electron version used.
    This compilation process is handled by `node-gyp` and may fail if required dependencies are missing or misconfigured.

    Refer to the [PowerSync Node package README](https://www.npmjs.com/package/@powersync/node) for more details.
  </Tab>

  <Tab title="Capacitor">
    Add the [PowerSync Capacitor NPM package](https://www.npmjs.com/package/@powersync/capacitor) to your project:

    <Tabs>
      <Tab title="npm">
        ```bash theme={null}
        npm install @powersync/capacitor
        ```
      </Tab>

      <Tab title="yarn">
        ```bash theme={null}
        yarn add @powersync/capacitor
        ```
      </Tab>

      <Tab title="pnpm">
        ```bash theme={null}
        pnpm install @powersync/capacitor
        ```
      </Tab>
    </Tabs>

    **Install Peer Dependencies**

    You must also install the following peer dependencies:

    <Tabs>
      <Tab title="npm">
        ```bash theme={null}
        npm install @capacitor-community/sqlite @powersync/web @journeyapps/wa-sqlite
        ```
      </Tab>

      <Tab title="yarn">
        ```bash theme={null}
        yarn add @capacitor-community/sqlite @powersync/web @journeyapps/wa-sqlite
        ```
      </Tab>

      <Tab title="pnpm">
        ```bash theme={null}
        pnpm install @capacitor-community/sqlite @powersync/web @journeyapps/wa-sqlite
        ```
      </Tab>
    </Tabs>

    After installing, sync your Capacitor project:

    ```bash theme={null}
    npx cap sync
    ```
  </Tab>

  <Tab title="Tauri">
    Add the [PowerSync Tauri NPM package](https://www.npmjs.com/package/@powersync/tauri-plugin) to your project:

    <Tabs>
      <Tab title="npm">
        ```bash theme={null}
        npm install @powersync/tauri-plugin
        ```
      </Tab>

      <Tab title="yarn">
        ```bash theme={null}
        yarn add @powersync/tauri-plugin
        ```
      </Tab>

      <Tab title="pnpm">
        ```bash theme={null}
        pnpm install @powersync/tauri-plugin
        ```
      </Tab>
    </Tabs>

    <Danger>
      Like all trusted PowerSync packages, the Tauri plugin is only available through the `@powersync/` scope on npm.
      The PowerSync Tauri plugin **cannot** be installed with the `tauri add` command.
    </Danger>

    Additionally, add the Tauri plugin crate to your Rust app (in the `src-tauri` directory):

    ```bash theme={null}
    cargo add tauri-plugin-powersync
    ```

    In your `lib.rs`, ensure the plugin is loaded:

    ```diff theme={null}
    pub fn run() {
        tauri::Builder::default()
            .invoke_handler(tauri::generate_handler![connect])
    +       .plugin(tauri_plugin_powersync::init())
            .run(tauri::generate_context!())
            .expect("error while running tauri application");
    }
    ```

    In `src-tauri/capabilities/default.json`, ensure `powersync:default` is listed under `permissions` to make PowerSync APIs available to JavaScript.
  </Tab>

  <Tab title="Dart/Flutter">
    Add the [PowerSync pub.dev package](https://pub.dev/packages/powersync) to your project:

    ```bash theme={null}
    dart pub add powersync
    ```
  </Tab>

  <Tab title="Kotlin">
    Add the [PowerSync SDK](https://central.sonatype.com/artifact/com.powersync/core) to your project by adding the following to your `build.gradle.kts` file:

    <Tabs sync={false}>
      <Tab title="With Version catalog">
        ```toml gradle/libs.versions.toml theme={null}
        [versions]
        # Please check the latest version at https://github.com/powersync-ja/powersync-kotlin/releases/
        powersync = "1.12.0"

        [libraries]
        powersync-core = { module = "com.powersync:core", version.ref = "powersync" }
        powersync-integration-supabase = { module = "com.powersync:connector-supabase", version.ref = "powersync" }
        ```

        ```Kotlin build.gradle.kts icon="https://mintcdn.com/powersync/GTJdSKFSfUc2Sxtc/logo/gradle.svg?fit=max&auto=format&n=GTJdSKFSfUc2Sxtc&q=85&s=bb14bd89bac7520f103a2ad2abc17053" theme={null}
        kotlin {
            //...
            sourceSets {
                commonMain.dependencies {
                    implementation(libs.powersync.core)
                    // If you want to use the Supabase Connector, also add the following:
                    implementation(libs.powersync.integration.supabase)
                }
                //...
            }
        }
        ```
      </Tab>

      <Tab title="Direct dependency">
        ```Kotlin build.gradle.kts icon="https://mintcdn.com/powersync/GTJdSKFSfUc2Sxtc/logo/gradle.svg?fit=max&auto=format&n=GTJdSKFSfUc2Sxtc&q=85&s=bb14bd89bac7520f103a2ad2abc17053" theme={null}
        kotlin {
            //...
            sourceSets {
                commonMain.dependencies {
                    implementation("com.powersync:core:$powersyncVersion")
                    // If you want to use the Supabase Connector, also add the following:
                    implementation("com.powersync:connector-supabase:$powersyncVersion")
                }
                //...
            }
        }
        ```
      </Tab>
    </Tabs>

    On Kotlin SDK v1.12.0 and later, the [PowerSync SQLite core extension](https://github.com/powersync-ja/powersync-sqlite-core) is statically linked into `com.powersync:core` for Apple targets (iOS, macOS, tvOS, and watchOS), consistent with Android and JVM. Use the Gradle dependencies above only. When you upgrade from an older SDK, remove any Swift package dependency on [`powersync-sqlite-core-swift`](https://github.com/powersync-ja/powersync-sqlite-core-swift) and any `powersync-sqlite-core` CocoaPod from your Xcode or CocoaPods setup.
  </Tab>

  <Tab title="Swift">
    You can add the PowerSync Swift package to your project using either `Package.swift` or Xcode:

    <Tabs>
      <Tab title="Package.swift">
        ```swift theme={null}
        let package = Package(
            //...
            dependencies: [
                //...
                .package(
                    url: "https://github.com/powersync-ja/powersync-swift",
                    exact: "<version>"
                ),
            ],
            targets: [
                .target(
                    name: "YourTargetName",
                    dependencies: [
                        .product(
                            name: "PowerSync",
                            package: "powersync-swift"
                        )
                    ]
                )
            ]
        )
        ```
      </Tab>

      <Tab title="Xcode">
        1. Follow [this guide](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app#Add-a-package-dependency) to add a package to your project.
        2. Use `https://github.com/powersync-ja/powersync-swift.git` as the URL
        3. Include the exact version (e.g., `1.0.x`)

        <Frame>
          <img src="https://mintcdn.com/powersync/lquPOu2QW4XM9BQW/images/Swift-Xcode.png?fit=max&auto=format&n=lquPOu2QW4XM9BQW&q=85&s=35c072838a7eb248e74b113b694e806d" width="2210" height="1212" data-path="images/Swift-Xcode.png" />
        </Frame>
      </Tab>
    </Tabs>
  </Tab>

  <Tab title=".NET">
    <Tabs>
      <Tab title="Common">
        For desktop/server/binary use-cases and WPF, add the [`PowerSync.Common`](https://www.nuget.org/packages/PowerSync.Common/) NuGet package to your project:

        ```bash theme={null}
        dotnet add package PowerSync.Common
        ```
      </Tab>

      <Tab title="MAUI">
        For MAUI apps, add both [`PowerSync.Common`](https://www.nuget.org/packages/PowerSync.Common/) and [`PowerSync.Maui`](https://www.nuget.org/packages/PowerSync.Maui/) NuGet packages to your project:

        ```bash theme={null}
        dotnet add package PowerSync.Common
        dotnet add package PowerSync.Maui
        ```
      </Tab>
    </Tabs>

    <Note>
      To install a specific version, use `--version` instead: `dotnet add package PowerSync.Common --version <version>`
    </Note>
  </Tab>

  <Tab title="Rust">
    Add the [PowerSync SDK](https://central.sonatype.com/artifact/com.powersync/core) to your project by adding the following to your `Cargo.toml` file:

    ```shell theme={null}
    cargo add powersync
    ```
  </Tab>
</Tabs>

### Define Your Client-Side Schema

This refers to the <Tooltip tip="The client-side schema is typically mainly derived from your backend source database schema and Sync Streams / Sync Rules, but can also include other tables such as local-only tables.">schema</Tooltip> for the managed SQLite database exposed by the PowerSync Client SDKs, that your app can read from and write to. The schema is applied when the database is instantiated (as we'll show in the next step) — <Tooltip tip="Schema migrations are not required on the SQLite database due to the schemaless nature of the PowerSync protocol. Schemaless data is synced to the client-side SQLite database, and the schema is then applied to that data using SQLite views to allow for structured querying of the data. The exception to this is if you are using Raw Tables (see Advanced section of Client SDK docs)">no migrations are required</Tooltip>.

*PowerSync Cloud:* The easiest way to generate your schema is using the [PowerSync Dashboard](https://dashboard.powersync.com/). Click the **Connect** button in the top bar to generate the client-side schema based on your Sync Streams in your preferred language.

Here's an example schema for a simple `todos` table:

<CodeGroup>
  ```typescript React Native (TS) theme={null}
  import { column, Schema, Table } from '@powersync/react-native';

  const todos = new Table(
    {
      list_id: column.text,
      created_at: column.text,
      completed_at: column.text,
      description: column.text,
      created_by: column.text,
      completed_by: column.text,
      completed: column.integer
    },
    { indexes: { list: ['list_id'] } }
  );

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

  ```typescript Web & Capacitor (TS) theme={null}
  import { column, Schema, Table } from '@powersync/web';

  const todos = new Table(
    {
      list_id: column.text,
      created_at: column.text,
      completed_at: column.text,
      description: column.text,
      created_by: column.text,
      completed_by: column.text,
      completed: column.integer
    },
    { indexes: { list: ['list_id'] } }
  );

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

  ```typescript Tauri (TS) theme={null}
  import { column, Schema, Table } from '@powersync/common';

  const todos = new Table(
    {
      list_id: column.text,
      created_at: column.text,
      completed_at: column.text,
      description: column.text,
      created_by: column.text,
      completed_by: column.text,
      completed: column.integer
    },
    { indexes: { list: ['list_id'] } }
  );

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

  ```typescript Node.js (TS) theme={null}
  import { column, Schema, Table } from '@powersync/node';

  const todos = new Table(
    {
      list_id: column.text,
      created_at: column.text,
      completed_at: column.text,
      description: column.text,
      created_by: column.text,
      completed_by: column.text,
      completed: column.integer
    },
    { indexes: { list: ['list_id'] } }
  );

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

  ```kotlin Kotlin theme={null}
  import com.powersync.db.schema.Column
  import com.powersync.db.schema.Schema
  import com.powersync.db.schema.Table
  import com.powersync.db.schema.Index
  import com.powersync.db.schema.IndexedColumn

  val AppSchema: Schema = Schema(
    listOf(
      Table(
        name = "todos",
        columns = listOf(
          Column.text("list_id"),
          Column.text("created_at"),
          Column.text("completed_at"),
          Column.text("description"),
          Column.integer("completed"),
          Column.text("created_by"),
          Column.text("completed_by")
        ),
        indexes = listOf(
          Index("list", listOf(IndexedColumn.descending("list_id")))
        )
      )
    )
  )
  ```

  ```swift Swift theme={null}
  import PowerSync

  let todos = Table(
    name: "todos",
    columns: [
      Column.text("list_id"),
      Column.text("description"),
      Column.integer("completed"),
      Column.text("created_at"),
      Column.text("completed_at"),
      Column.text("created_by"),
      Column.text("completed_by")
    ],
    indexes: [
      Index(
        name: "list_id",
        columns: [IndexedColumn.ascending("list_id")]
      )
    ]
  )

  let AppSchema = Schema(todos)
  ```

  ```dart Dart/Flutter theme={null}
  import 'package:powersync/powersync.dart';

  const schema = Schema(([
    Table('todos', [
      Column.text('list_id'),
      Column.text('created_at'),
      Column.text('completed_at'),
      Column.text('description'),
      Column.integer('completed'),
      Column.text('created_by'),
      Column.text('completed_by'),
    ], indexes: [
      Index('list', [IndexedColumn('list_id')])
    ])
  ]));
  ```

  ```csharp .NET theme={null}
  using PowerSync.Common.DB.Schema;
  using PowerSync.Common.DB.Schema.Attributes;

  [Table("todos"), Index("list", ["list_id"])]
  public class Todo
  {
      // Attribute-based schema requires an explicit id; other syntaxes define an implicit id key. Learn more in the .NET SDK reference.
      [Column("id")]
      public string TodoId { get; set; }

      [Column("list_id")]
      public string ListId { get; set; }

      [Column("created_at")]
      public string CreatedAt { get; set; }

      [Column("completed_at")]
      public string CompletedAt { get; set; }

      [Column("description")]
      public string Description { get; set; }

      [Column("created_by")]
      public string CreatedBy { get; set; }

      [Column("completed_by")]
      public string CompletedBy { get; set; }

      [Column("completed")]
      public bool Completed { get; set; }
  }

  public static Schema PowerSyncSchema = new Schema(typeof(Todo));
  ```

  <Note>This uses the recommended attribute-based syntax, where your C# class doubles as both the schema definition and the result type for queries — so you only define your data structure once. If you prefer to keep your schema definition separate from your data classes, an object initializer syntax is also available. See the [.NET SDK reference](/client-sdks/reference/dotnet#schema-definition-syntax) for details.</Note>

  ```rust Rust theme={null}
  use powersync::schema::{Column, Schema, Table};

  pub fn app_schema() -> Schema {
      let mut schema = Schema::default();
      let todos = Table::create(
          "todos",
          vec![
              Column::text("list_id"),
              Column::text("created_at"),
              Column::text("completed_at"),
              Column::text("description"),
              Column::integer("completed"),
              Column::text("created_by"),
              Column::text("completed_by"),
          ],
          |_| {},
      );
      schema.tables.push(todos);
      schema
  }
  ```
</CodeGroup>

<Note>
  **Note**: The schema does not explicitly specify an `id` column, since PowerSync automatically creates an `id` column of type `text`. PowerSync [recommends](/sync/advanced/client-id) using UUIDs.
</Note>

<Tip>
  **Learn More**

  The client-side schema uses three column types: `text`, `integer`, and `real`. These map directly to values from your Sync Streams and are automatically cast if needed. For details on how backend database types map to SQLite types, see [Types](/sync/types).
</Tip>

### Instantiate the PowerSync Database

Now that you have your client-side schema defined, instantiate the PowerSync database in your app. This creates the client-side SQLite database that will be kept in sync with your source database based on your Sync Streams.

<CodeGroup>
  ```typescript React Native (TS) theme={null}
  import { PowerSyncDatabase } from '@powersync/react-native';
  import { AppSchema } from './Schema';

  export const db = new PowerSyncDatabase({
    schema: AppSchema,
    database: {
      dbFilename: 'powersync.db'
    }
  });
  ```

  ```typescript Web (TS) theme={null}
  import { PowerSyncDatabase } from '@powersync/web';
  import { AppSchema } from './Schema';

  export const db = new PowerSyncDatabase({
    schema: AppSchema,
    database: {
      dbFilename: 'powersync.db'
    }
  });
  ```

  ```typescript Node.js (TS) theme={null}
  import { PowerSyncDatabase } from '@powersync/node';
  import { AppSchema } from './Schema';

  export const db = new PowerSyncDatabase({
    schema: AppSchema,
    database: {
      dbFilename: 'powersync.db'
    }
  });
  ```

  ```typescript Tauri (TS) theme={null}
  import { PowerSyncTauriDatabase } from '@powersync/tauri-plugin';
  import { appDataDir } from '@tauri-apps/api/path';
  import { AppSchema } from './AppSchema';

  export const db = new PowerSyncTauriDatabase({
    schema: AppSchema,
    database: {
      dbFilename: 'powersync.db',
      // Store the database in the app data directory
      dbLocationAsync: appDataDir,
    }
  });
  ```

  ```typescript Capacitor (TS) theme={null}
  import { PowerSyncDatabase } from '@powersync/capacitor';

  // Import general components from the Web SDK package
  import { Schema } from '@powersync/web';
  import { Connector } from './Connector';
  import { AppSchema } from './AppSchema';

  /**
   * The Capacitor PowerSyncDatabase will automatically detect the platform
   * and use the appropriate database drivers.
   */
  export const db = new PowerSyncDatabase({
    // The schema you defined in the previous step
    schema: AppSchema,
    database: {
      // Filename for the SQLite database — it's important to only instantiate one instance per file.
      dbFilename: 'powersync.db'
    }
  });
  ```

  ```kotlin Kotlin theme={null}
  import com.powersync.DatabaseDriverFactory
  import com.powersync.PowerSyncDatabase

  // Android
  val driverFactory = DatabaseDriverFactory(this)
  // iOS & Desktop
  // val driverFactory = DatabaseDriverFactory()

  val database = PowerSyncDatabase({
    factory: driverFactory,
    schema: AppSchema,
    dbFilename: "powersync.db"
  })
  ```

  ```swift Swift theme={null}
  import PowerSync

  let db = PowerSyncDatabase(
    schema: AppSchema,
    dbFilename: "powersync.sqlite"
  )
  ```

  ```dart Dart/Flutter theme={null}
  import 'package:powersync/powersync.dart';
  import 'package:path_provider/path_provider.dart';
  import 'package:path/path.dart';

  openDatabase() async {
    final dir = await getApplicationSupportDirectory();
    final path = join(dir.path, 'powersync-dart.db');
    db = PowerSyncDatabase(schema: schema, path: path);
    await db.initialize();
  }
  ```

  ```csharp .NET - Common theme={null}
  using PowerSync.Common.Client;

  class Demo
  {
      static async Task Main()
      {
          var db = new PowerSyncDatabase(new PowerSyncDatabaseOptions
          {
              Database = new SQLOpenOptions { DbFilename = "tododemo.db" },
              Schema = AppSchema.PowerSyncSchema,
          });
          await db.Init();
      }
  }
  ```

  ```csharp .NET - MAUI theme={null}
  using PowerSync.Common.Client;
  using PowerSync.Common.MDSQLite;
  using PowerSync.Maui.SQLite;

  class Demo
  {
      static async Task Main() 
      {
          // Ensures the DB file is stored in a platform appropriate location
          var dbPath = Path.Combine(FileSystem.AppDataDirectory, "maui-example.db");
          var factory = new MAUISQLiteDBOpenFactory(new MDSQLiteOpenFactoryOptions()
          {
              DbFilename = dbPath
          });

          var Db = new PowerSyncDatabase(new PowerSyncDatabaseOptions()
          {
              Database = factory, // Supply a factory
              Schema = AppSchema.PowerSyncSchema,
          });

          await db.Init();
      }
  }
  ```

  ```rust Rust theme={null}
  // 1. Process setup: register PowerSync extension early (e.g. in main()).
  // 2. Open a connection pool, create env, then database. Spawn async tasks
  //    before connecting (see Connect step). Requires powersync with tokio feature.

  use powersync::{ConnectionPool, PowerSyncDatabase, error::PowerSyncError};
  use powersync::env::PowerSyncEnvironment;
  use std::sync::Arc;
  use http_client::IsahcClient;

  fn open_pool() -> Result<ConnectionPool, PowerSyncError> {
      ConnectionPool::open("powersync.db")
  }

  // This example shows the Tokio runtime. You must call 
  // `PowerSyncEnvironment::powersync_auto_extension()` before using the SDK and spawn async 
  // tasks with `db.async_tasks().spawn_with_tokio()` (or `spawn_with` for other runtimes) 
  // before connecting. See the Rust SDK reference for in-memory pools, smol, or custom runtimes.

  #[tokio::main]
  async fn main() {
      PowerSyncEnvironment::powersync_auto_extension()
          .expect("could not load PowerSync core extension");

      let pool = open_pool().expect("open pool");
      let client = Arc::new(IsahcClient::new());
      let env = PowerSyncEnvironment::custom(
          client.clone(),
          pool,
          Box::new(PowerSyncEnvironment::tokio_timer()),
      );

      let db = PowerSyncDatabase::new(env, app_schema());
      db.async_tasks().spawn_with_tokio();
      // Connect with a backend connector in the next step.
  }
  ```
</CodeGroup>

### Connect to PowerSync Service Instance

Connect your client-side PowerSync database to the PowerSync Service instance you created in [step 2](#2-set-up-powersync-service-instance) by defining a *backend connector* and calling `connect()`. The backend connector handles authentication and uploading mutations to your backend.

<Tip>
  **Note**: This section assumes you want to use PowerSync to sync your backend source database with SQLite in your app. If you only want to use PowerSync to manage your local SQLite database without sync, instantiate the PowerSync database without calling `connect()` and refer to our [Local-Only](/client-sdks/advanced/local-only-usage) guide.
</Tip>

<Note>You don't have to worry about the *backend connector* implementation details right now — you can leave the boilerplate as-is and come back to it later.</Note>

For development, you can use the development token you generated in the [Generate a Development Token](#5-generate-a-development-token) step above. For production, you'll implement proper JWT authentication as we'll explain further below.

<CodeGroup>
  ```typescript React Native (TS) theme={null}
  import { AbstractPowerSyncDatabase, PowerSyncBackendConnector, PowerSyncCredentials } from '@powersync/react-native';
  import { db } from './Database';

  class Connector implements PowerSyncBackendConnector {
    async fetchCredentials(): Promise<PowerSyncCredentials> {
      // for development: use development token
      return {
        endpoint: 'https://your-instance.powersync.com',
        token: 'your-development-token-here'
      };
    }

    async uploadData(database: AbstractPowerSyncDatabase) {
      const transaction = await database.getNextCrudTransaction();
      if (!transaction) return;

      for (const op of transaction.crud) {
        const record = { ...op.opData, id: op.id };
        // upload to your backend API
      }

      await transaction.complete();
    }
  }

  // connect the database to PowerSync Service
  const connector = new Connector();
  await db.connect(connector);
  ```

  ```typescript Web & Capacitor (TS) theme={null}
  import { AbstractPowerSyncDatabase, PowerSyncBackendConnector, PowerSyncCredentials } from '@powersync/web';
  import { db } from './Database';

  class Connector implements PowerSyncBackendConnector {
    async fetchCredentials(): Promise<PowerSyncCredentials> {
      // for development: use development token
      return {
        endpoint: 'https://your-instance.powersync.com',
        token: 'your-development-token-here'
      };
    }

    async uploadData(database: AbstractPowerSyncDatabase) {
      const transaction = await database.getNextCrudTransaction();
      if (!transaction) return;

      for (const op of transaction.crud) {
        const record = { ...op.opData, id: op.id };
        // upload to your backend API
      }

      await transaction.complete();
    }
  }

  // connect the database to PowerSync Service
  const connector = new Connector();
  await db.connect(connector);
  ```

  ```typescript Node.js (TS) theme={null}
  import { PowerSyncBackendConnector } from '@powersync/node';

  export class Connector implements PowerSyncBackendConnector {
      async fetchCredentials() {
          // for development: use development token
          return {
              endpoint: 'https://your-instance.powersync.com',
              token: 'your-development-token-here'
          };
      }

      async uploadData(database) {
        // upload to your backend API
      }
  }

  // connect the database to PowerSync Service
  const connector = new Connector();
  await db.connect(connector);
  ```

  ```kotlin Kotlin theme={null}
  import com.powersync.PowerSyncCredentials
  import com.powersync.PowerSyncDatabase

  class MyConnector : PowerSyncBackendConnector {
    override suspend fun fetchCredentials(): PowerSyncCredentials {
      // for development: use development token
      return PowerSyncCredentials(
        endpoint = "https://your-instance.powersync.com",
        token = "your-development-token-here"
      )
    }

    override suspend fun uploadData(database: PowerSyncDatabase) {
      val transaction = database.getNextCrudTransaction() ?: return
      
      for (op in transaction.crud) {
        val record = op.opData + ("id" to op.id)
        // upload to your backend API
      }
      
      transaction.complete()
    }
  }

  // connect the database to PowerSync Service
  database.connect(MyConnector())
  ```

  ```swift Swift theme={null}
  import PowerSync

  class Connector: PowerSyncBackendConnector {
    func fetchCredentials() async throws -> PowerSyncCredentials {
      // for development: use development token
      return PowerSyncCredentials(
        endpoint: "https://your-instance.powersync.com",
        token: "your-development-token-here"
      )
    }

    func uploadData(database: PowerSyncDatabase) async throws {
      guard let transaction = try await database.getNextCrudTransaction() else {
        return
      }
      
      for op in transaction.crud {
        var record = op.opData
        record["id"] = op.id
        // upload to your backend API
      }
      
      try await transaction.complete()
    }
  }

  // connect the database to PowerSync Service
  let connector = Connector()
  await db.connect(connector: connector)
  ```

  ```dart Dart/Flutter theme={null}
  import 'package:powersync/powersync.dart';

  class Connector extends PowerSyncBackendConnector {
    @override
    Future<PowerSyncCredentials> fetchCredentials() async {
      return PowerSyncCredentials(
        endpoint: 'https://your-instance.powersync.com',
        token: 'your-development-token-here'
      );
    }

    @override
    Future<void> uploadData(PowerSyncDatabase database) async {
      final transaction = await database.getNextCrudTransaction();
      if (transaction == null) return;

      for (final op in transaction.crud) {
        final record = {...op.opData, 'id': op.id};
        // upload to your backend API
      }

      await transaction.complete();
    }
  }

  // connect the database to PowerSync Service
  final connector = Connector();
  await db.connect(connector);
  ```

  ```csharp .NET theme={null}
  using System;
  using System.Collections.Generic;
  using System.Net.Http;
  using System.Text;
  using System.Text.Json;
  using System.Threading.Tasks;
  using PowerSync.Common.Client;
  using PowerSync.Common.Client.Connection;
  using PowerSync.Common.DB.Crud;

  public class MyConnector : IPowerSyncBackendConnector
  {
      public MyConnector()
      {
      }

      public async Task<PowerSyncCredentials?> FetchCredentials()
      {
          var powerSyncUrl = "https://your-instance.powersync.com";
          var authToken = "your-development-token-here";

          // Return credentials with PowerSync endpoint and JWT token
          return new PowerSyncCredentials(powerSyncUrl, authToken);
      }

      public async Task UploadData(IPowerSyncDatabase database)
      {
          // upload to your backend API
      }
  }

  // connect the database to PowerSync Service
  await db.Connect(new MyConnector());
  ```

  ```rust Rust theme={null}
  use async_trait::async_trait;
  use powersync::{BackendConnector, PowerSyncCredentials, PowerSyncDatabase, SyncOptions};
  use powersync::error::PowerSyncError;
  use std::sync::Arc;

  struct MyBackendConnector {
      client: Arc<dyn http_client::HttpClient>,
      db: PowerSyncDatabase,
  }

  #[async_trait]
  impl BackendConnector for MyBackendConnector {
      async fn fetch_credentials(&self) -> Result<PowerSyncCredentials, PowerSyncError> {
          // for development: use development token
          Ok(PowerSyncCredentials {
              endpoint: "https://your-instance.powersync.com".to_string(),
              token: "your-development-token-here".to_string(),
          })
      }

      async fn upload_data(&self) -> Result<(), PowerSyncError> {
          let mut local_writes = self.db.crud_transactions();
          while let Some(tx) = local_writes.try_next().await? {
              // upload to your backend API
              tx.complete().await?;
          }
          Ok(())
      }
  }

  // connect the database to PowerSync Service
  db.connect(SyncOptions::new(MyBackendConnector {
      client,
      db: db.clone(),
  }))
  .await;
  ```

  ```rust Tauri (Rust) theme={null}
  // For Tauri, connecting to PowerSync must be done in Rust so that sync state
  // is shared across all windows. Calling connect() from JavaScript will throw.
  //
  // 1. Define your backend connector in Rust:

  use async_trait::async_trait;
  use tauri_plugin_powersync::PowerSyncExt;
  use powersync::{BackendConnector, PowerSyncCredentials, PowerSyncDatabase, SyncOptions};
  use powersync::error::PowerSyncError;

  struct MyBackendConnector {
      db: PowerSyncDatabase,
  }

  #[async_trait]
  impl BackendConnector for MyBackendConnector {
      async fn fetch_credentials(&self) -> Result<PowerSyncCredentials, PowerSyncError> {
          // for development: use development token
          Ok(PowerSyncCredentials {
              endpoint: "https://your-instance.powersync.com".to_string(),
              token: "your-development-token-here".to_string(),
          })
      }

      async fn upload_data(&self) -> Result<(), PowerSyncError> {
          let mut local_writes = self.db.crud_transactions();
          while let Some(tx) = local_writes.try_next().await? {
              // upload to your backend API
              tx.complete().await?;
          }
          Ok(())
      }
  }

  // 2. Add a Tauri command that receives the database handle from JavaScript:
  #[tauri::command]
  async fn connect<R: tauri::Runtime>(
      app: tauri::AppHandle<R>,
      handle: usize,
  ) -> tauri_plugin_powersync::Result<()> {
      let database = app.powersync().database_from_javascript_handle(handle)?;
      let options = SyncOptions::new(MyBackendConnector { db: database.clone() });
      database.connect(options).await;
      Ok(())
  }

  // 3. Register the command in your Tauri builder:
  // .invoke_handler(tauri::generate_handler![connect])

  // 4. Then call it from JavaScript after initializing the database:
  // await db.init();
  // await invoke('connect', { handle: db.rustHandle });
  ```
</CodeGroup>

Once connected, you can read from and write to the client-side SQLite database. Changes from your source database will be automatically synced down into the SQLite database. For client-side mutations to be uploaded back to your source database, you need to complete the backend integration as we'll explain below.

### Read Data

Read data using SQL queries. The data comes from your client-side SQLite database:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Get all todos
  const todos = await db.getAll('SELECT * FROM todos');

  // Get a single todo
  const todo = await db.get('SELECT * FROM todos WHERE id = ?', [todoId]);

  // Watch for changes (reactive query)
  const stream = db.watch('SELECT * FROM todos WHERE list_id = ?', [listId]);
  for await (const todos of stream) {
    // Update UI when data changes
    console.log(todos);
  }

  // Note: The above example requires async iterator support in React Native. 
  // If you encounter issues, use one of these callback-based APIs instead:

  // Option 1: Using onResult callback
  // const abortController = new AbortController();
  // db.watch(
  //   'SELECT * FROM todos WHERE list_id = ?',
  //   [listId],
  //   {
  //     onResult: (todos) => {
  //       // Update UI when data changes
  //       console.log(todos);
  //     }
  //   },
  //   { signal: abortController.signal }
  // );

  // Option 2: Using the query builder API
  // const query = db
  //   .query({
  //     sql: 'SELECT * FROM todos WHERE list_id = ?',
  //     parameters: [listId]
  //   })
  //   .watch();
  // query.registerListener({
  //   onData: (todos) => {
  //     // Update UI when data changes
  //     console.log(todos);
  //   }
  // });
  ```

  ```kotlin Kotlin theme={null}
  // Get all todos
  val todos = database.getAll("SELECT * FROM todos") { cursor ->
    Todo.fromCursor(cursor)
  }

  // Get a single todo
  val todo = database.get("SELECT * FROM todos WHERE id = ?", listOf(todoId)) { cursor ->
    Todo.fromCursor(cursor)
  }

  // Watch for changes
  database.watch("SELECT * FROM todos WHERE list_id = ?", listOf(listId))
    .collect { todos ->
      // Update UI when data changes
    }
  ```

  ```swift Swift theme={null}
  // Get all todos
  let todos = try await db.getAll(
    sql: "SELECT * FROM todos",
    mapper: { cursor in
      TodoContent(
        description: try cursor.getString(name: "description")!,
        completed: try cursor.getBooleanOptional(name: "completed")
      )
    }
  )

  // Watch for changes
  for try await todos in db.watch(
    sql: "SELECT * FROM todos WHERE list_id = ?",
    parameters: [listId]
  ) {
    // Update UI when data changes
  }
  ```

  ```dart Dart/Flutter theme={null}
  // Get all todos
  final todos = await db.getAll('SELECT * FROM todos');

  // Get a single todo
  final todo = await db.get('SELECT * FROM todos WHERE id = ?', [todoId]);

  // Watch for changes
  db.watch('SELECT * FROM todos WHERE list_id = ?', [listId])
    .listen((todos) {
      // Update UI when data changes
    });
  ```

  ```csharp .NET theme={null}
  // Define a result type with properties matching schema columns (some columns omitted for brevity)
  // public class ListResult { public string id; public string name; public string owner_id; public string created_at; ... }

  // Use db.Get() to fetch a single row:
  var list = await db.Get<ListResult>("SELECT * FROM lists WHERE id = ?", [listId]);

  // Use db.GetAll() to fetch all rows:
  var lists = await db.GetAll<ListResult>("SELECT * FROM lists");

  // Watch for changes to query results
  var query = await db.Watch("SELECT * FROM lists", null, new WatchHandler<ListResult>
  {
      OnResult = (results) => Console.WriteLine($"Lists updated: {results.Length} items"),
      OnError = (error) => Console.WriteLine($"Error: {error.Message}")
  });

  // Call query.Dispose() to stop watching for updates
  query.Dispose();
  ```

  ```rust Rust theme={null}
  use rusqlite::params;
  use futures::StreamExt; // for try_next() on the watch stream

  // Get all todos
  async fn get_all_todos(db: &PowerSyncDatabase) -> Result<(), PowerSyncError> {
      let reader = db.reader().await?;
      let mut stmt = reader.prepare("SELECT * FROM todos")?;
      let mut rows = stmt.query(params![])?;
      while let Some(row) = rows.next()? {
          let id: String = row.get("id")?;
          let description: String = row.get("description")?;
          // use row data
      }
      Ok(())
  }

  // Get a single todo
  async fn find_todo(db: &PowerSyncDatabase, todo_id: &str) -> Result<(), PowerSyncError> {
      let reader = db.reader().await?;
      let mut stmt = reader.prepare("SELECT * FROM todos WHERE id = ?")?;
      let mut rows = stmt.query(params![todo_id])?;
      while let Some(row) = rows.next()? {
          let id: String = row.get("id")?;
          let description: String = row.get("description")?;
          println!("Found todo: {id}, {description}");
      }
      Ok(())
  }

  // Watch for changes
  async fn watch_todos(db: &PowerSyncDatabase, list_id: &str) -> Result<(), PowerSyncError> {
      let stream = db.watch_statement(
          "SELECT * FROM todos WHERE list_id = ?".to_string(),
          params![list_id],
          |stmt, params| {
              let mut rows = stmt.query(params)?;
              let mut mapped = vec![];
              while let Some(row) = rows.next()? {
                  mapped.push(() /* TODO: Read row into struct */);
              }
              Ok(mapped)
          },
      );
      let mut stream = std::pin::pin!(stream);
      while let Some(_event) = stream.try_next().await? {
          // Update UI when data changes
      }
      Ok(())
  }
  ```
</CodeGroup>

<Tip>
  **Learn More**

  * [Reading Data](/client-sdks/reading-data) - Details on querying synced data
  * [ORMs Overview](/client-sdks/orms/overview) - Using type-safe ORMs with PowerSync
  * [Live Queries / Watch Queries](/client-sdks/watch-queries) - Building reactive UIs with automatic updates
</Tip>

### Write Data

Write data using SQL `INSERT`, `UPDATE`, or `DELETE` statements. PowerSync automatically queues these mutations and uploads them to your backend via the `uploadData()` function, once you've fully implemented your *backend connector* (as we'll talk about below).

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Insert a new todo
  await db.execute(
    'INSERT INTO todos (id, created_at, list_id, description) VALUES (uuid(), date(), ?, ?)',
    [listId, 'Buy groceries']
  );

  // Update a todo
  await db.execute(
    'UPDATE todos SET completed = 1, completed_at = date() WHERE id = ?',
    [todoId]
  );

  // Delete a todo
  await db.execute('DELETE FROM todos WHERE id = ?', [todoId]);
  ```

  ```kotlin Kotlin theme={null}
  // Insert a new todo
  database.writeTransaction { ctx ->
    ctx.execute(
      sql = "INSERT INTO todos (id, created_at, list_id, description) VALUES (uuid(), date(), ?, ?)",
      parameters = listOf(listId, "Buy groceries")
    )
  }

  // Update a todo
  database.execute(
    sql = "UPDATE todos SET completed = 1, completed_at = date() WHERE id = ?",
    parameters = listOf(todoId)
  )

  // Delete a todo
  database.execute(
    sql = "DELETE FROM todos WHERE id = ?",
    parameters = listOf(todoId)
  )
  ```

  ```swift Swift theme={null}
  // Insert a new todo
  try await db.execute(
    sql: "INSERT INTO todos (id, created_at, list_id, description) VALUES (uuid(), date(), ?, ?)",
    parameters: [listId, "Buy groceries"]
  )

  // Update a todo
  try await db.execute(
    sql: "UPDATE todos SET completed = 1, completed_at = date() WHERE id = ?",
    parameters: [todoId]
  )

  // Delete a todo
  try await db.execute(
    sql: "DELETE FROM todos WHERE id = ?",
    parameters: [todoId]
  )
  ```

  ```dart Dart/Flutter theme={null}
  // Insert a new todo
  await db.execute(
    'INSERT INTO todos (id, created_at, list_id, description) VALUES (uuid(), date(), ?, ?)',
    [listId, 'Buy groceries']
  );

  // Update a todo
  await db.execute(
    'UPDATE todos SET completed = 1, completed_at = date() WHERE id = ?',
    [todoId]
  );

  // Delete a todo
  await db.execute('DELETE FROM todos WHERE id = ?', [todoId]);
  ```

  ```csharp .NET theme={null}
  // Insert a new todo
  await db.Execute(
    "INSERT INTO todos (id, created_at, list_id, description) VALUES (uuid(), datetime(), ?, ?)",
    [listId, "Buy groceries"]
  );

  // Update a todo
  await db.Execute(
    "UPDATE todos SET completed = 1, completed_at = datetime() WHERE id = ?",
    [todoId]
  );

  // Delete a todo
  await db.Execute("DELETE FROM todos WHERE id = ?", [todoId]);
  ```

  ```rust Rust theme={null}
  use rusqlite::params;

  // Insert a new todo
  async fn insert_todo(
      db: &PowerSyncDatabase,
      list_id: &str,
      description: &str,
  ) -> Result<(), PowerSyncError> {
      let writer = db.writer().await?;
      writer.execute(
          "INSERT INTO todos (id, created_at, list_id, description) VALUES (uuid(), date(), ?, ?)",
          params![list_id, description],
      )?;
      Ok(())
  }

  // Update a todo
  async fn complete_todo(db: &PowerSyncDatabase, todo_id: &str) -> Result<(), PowerSyncError> {
      let writer = db.writer().await?;
      writer.execute(
          "UPDATE todos SET completed = 1, completed_at = date() WHERE id = ?",
          params![todo_id],
      )?;
      Ok(())
  }

  // Delete a todo
  async fn delete_todo(db: &PowerSyncDatabase, todo_id: &str) -> Result<(), PowerSyncError> {
      let writer = db.writer().await?;
      writer.execute("DELETE FROM todos WHERE id = ?", params![todo_id])?;
      Ok(())
  }
  ```
</CodeGroup>

<Note>
  **Best practice**: Use UUIDs when inserting new rows on the client side. UUIDs can be generated offline/locally, allowing for unique identification of records created in the client database before they are synced to the server. See [Client ID](/sync/advanced/client-id) for more details.
</Note>

<Tip>
  **Learn More**

  For more details, see the [Writing Data](/client-sdks/writing-data) page.
</Tip>

# Next Steps

For production deployments, you'll need to:

1. **[Implement Authentication](/configuration/auth/overview)**: Replace development tokens with proper JWT-based authentication. PowerSync supports various authentication providers including Supabase, Firebase Auth, Auth0, Clerk, and custom JWT implementations.
2. **Configure & Integrate Your Backend Application**: Set up your backend to handle mutations uploaded from clients.
   * [Server-Side Setup](/configuration/app-backend/setup)
   * [Client-Side Integration](/configuration/app-backend/client-side-integration)

### Additional Resources

* Learn more about [Sync Streams](/sync/streams/overview) for controlling partial syncing.
* Explore [Live Queries / Watch Queries](/client-sdks/watch-queries) for reactive UI updates.
* Check out [Example Projects](/intro/examples) for complete implementations.
* Review the [Client SDK References](/client-sdks/overview) for client-side platform-specific details.

# Questions?

Try "Ask AI" on this site which is trained on all our documentation, repositories and Discord discussions. Also join us on [our community Discord server](https://discord.gg/powersync) where you can browse topics from the PowerSync community and chat with our team.
