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

# Client-Side Integration With Your Backend

> Connect your client app to a PowerSync-compatible backend using the connector interface and upload queue.

## How PowerSync Uses Your Backend

After you've [instantiated](/intro/setup-guide#instantiate-the-powersync-database) the client-side PowerSync database, you will call `connect()` on it, which causes the PowerSync Client SDK to connect to the [PowerSync Service](/architecture/powersync-service) for the purpose of syncing data to the client-side SQLite database, *and* to connect to your backend application as needed, for two potential purposes:

| Purpose                                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Uploading mutations to your backend:**   | Mutations that are made to the client-side SQLite database are uploaded to your backend application, where you control how they're applied to your backend source database (Postgres, MongoDB, MySQL, or SQL Server). This is how PowerSync achieves bi-directional syncing of data: The [PowerSync Service](/architecture/powersync-service) provides the *server-to-client read path* based on your [Sync Streams or Sync Rules (legacy)](/sync/overview), and the *client-to-server write path* goes via your backend. |
| **Authentication integration:** (optional) | PowerSync uses JWTs for authentication between the Client SDK and PowerSync Service. Some [authentication providers](/configuration/auth/overview#common-authentication-providers) generate JWTs for users which PowerSync can verify directly. For others, some code must be [added to your application backend](/configuration/auth/custom) to generate the JWTs.                                                                                                                                                       |

## 'Backend Connector'

Accordingly, you must pass a *backend connector* as an argument when you call `connect()` on the client-side PowerSync database. You must define that backend connector, and it must implement two functions/methods:

| Purpose                                  | Function             | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| ---------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Uploading mutations to your backend:** | `uploadData()`       | The PowerSync Client SDK automatically calls this function to upload client-side mutations to your backend. Whenever you write to the client-side SQLite database, those writes are also automatically placed into an *upload queue* by the Client SDK, and the Client SDK processes the entries in the upload queue by calling `uploadData()`. You should define your `uploadData()` function to call your backend application API to upload and apply the write operations to your backend source database. The Client SDK automatically handles retries in the case of failures. See the [detailed behavior below](#when-uploaddata-is-called) and [Writing Data](/client-sdks/writing-data) in the *Client SDKs* section for more details on the implementation of `uploadData()`. |
| **Authentication integration:**          | `fetchCredentials()` | Called by the PowerSync Client SDK to obtain a JWT and the endpoint URL for your PowerSync Service instance. The SDK uses the JWT to authenticate against the PowerSync Service. `fetchCredentials()` <Tooltip tip="The specifics can vary slightly depending on which PowerSync Client SDK you are using">typically</Tooltip> returns an object with `token` (JWT) and `endpoint` fields. See [Authentication Setup](/configuration/auth/overview) for more details on JWT authentication.                                                                                                                                                                                                                                                                                            |

<Note>Some authentication providers generate JWTs for users which PowerSync can verify directly, and in that case, your `fetchCredentials()` function implementation can simply return that JWT from client-side state. Your `fetchCredentials()` implementation only needs to retrieve a JWT from your backend if you are using [Custom Authentication](/configuration/auth/custom) integration. See the [Authentication Overview](/configuration/auth/overview) for more background.</Note>

### When `uploadData()` is Called

The PowerSync Client SDK calls `uploadData()` automatically - you never call it directly. It is invoked in the following scenarios:

1. **After a local write** - Any `INSERT`, `UPDATE`, or `DELETE` on a PowerSync table adds an entry to the internal upload queue (the `ps_crud` table). The SDK monitors this table for changes and triggers `uploadData()` shortly after. See [How Local Writes Are Detected](#how-local-writes-are-detected) below for details on the underlying mechanism.
2. **On initial connection / reconnection** - When `connect()` establishes (or re-establishes) a sync stream and the first message is received from the PowerSync Service, the SDK triggers `uploadData()` to flush any writes that were made while offline or disconnected.
3. **On keepalive messages** - The PowerSync Service sends periodic keepalive messages (every 20 seconds, with slight jitter). Each keepalive triggers an upload attempt, ensuring pending writes are retried even if no new local writes have occurred.
4. **After an error, with retry** - If `uploadData()` throws an error, the SDK waits for a configurable delay (default: 5 seconds, controlled by `retryDelayMs`) and then retries. This continues until the upload succeeds or the sync stream is disconnected.

#### How Local Writes Are Detected

By default, each PowerSync-managed table is exposed in SQLite as a **view** with `INSTEAD OF INSERT/UPDATE/DELETE` triggers (generated by the [PowerSync SQLite core extension](https://github.com/powersync-ja/powersync-sqlite-core) when your [client-side schema](/intro/setup-guide#define-your-client-side-schema) is applied). When you write to a PowerSync table, those triggers do two things atomically:

1. Apply the change to the [underlying `ps_data__<table>` table](/architecture/client-architecture#client-side-schema-and-sqlite-database-structure).
2. Append an entry to `ps_crud` describing the operation (see [Write Operations and Upload Queue](/client-sdks/writing-data#write-operations-and-upload-queue) for the entry format).

Because both happen in the same transaction, the upload queue can never get out of sync with local data. ([Raw Tables](/client-sdks/advanced/raw-tables#capture-local-writes-with-triggers) preserve this guarantee, except you create the triggers yourself, typically via the `powersync_create_raw_table_crud_trigger` helper.)

The SDK then detects new `ps_crud` entries by subscribing to SQLite's table-update notifications, filtered for changes to `ps_crud`. This is the same mechanism that powers reactive [watch queries](/client-sdks/watch-queries). When a change is observed, the SDK schedules an `uploadData()` call subject to the configured [throttle interval](#throttling).

The mechanism is consistent across all PowerSync SDKs (JavaScript, Dart, Kotlin, Swift, .NET, Rust), since the triggers and `ps_crud` table are defined by the shared PowerSync SQLite core extension.

#### Upload Loop Behavior

The SDK calls `uploadData()` in a **loop** - not just once per trigger. After each successful `uploadData()` call, the SDK checks whether there are more items in the upload queue. If there are, it calls `uploadData()` again immediately. The loop continues until the queue is empty. This means your `uploadData()` implementation only needs to process one batch (or one transaction) per call.

Once the queue is empty, the SDK updates an internal write checkpoint used for [consistency](/architecture/consistency) tracking.

#### Throttling

To avoid excessive calls, upload triggers are **throttled**. Rapid local writes (e.g., multiple `INSERT` statements in quick succession) are coalesced so that `uploadData()` is invoked at most once per throttle interval. If an upload is already in progress when a new write occurs, the SDK will trigger another upload after the current one completes.

The default throttle interval varies by SDK:

| SDK                                                  | Default  | Option Name              |
| ---------------------------------------------------- | -------- | ------------------------ |
| JavaScript / React Native / Node / Capacitor / Tauri | 1,000 ms | `crudUploadThrottleMs`   |
| Kotlin                                               | 1,000 ms | `crudThrottleMs`         |
| Swift                                                | 1 second | `crudThrottle` (seconds) |
| .NET                                                 | 1,000 ms | `CrudUploadThrottleMs`   |
| Dart / Flutter                                       | 10 ms    | `crudThrottleTime`       |

The throttle interval is configurable via options when calling `connect()`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  await db.connect(connector, { crudUploadThrottleMs: 500 });
  ```

  ```dart Dart/Flutter theme={null}
  db.connect(
    connector: connector,
    options: SyncOptions(
      crudThrottleTime: Duration(milliseconds: 500),
    ),
  );
  ```

  ```kotlin Kotlin theme={null}
  database.connect(connector, crudThrottleMs = 500L)
  ```

  ```swift Swift theme={null}
  try await database.connect(
    connector: connector,
    options: ConnectOptions(crudThrottle: 0.5) // seconds
  )
  ```

  ```csharp .NET theme={null}
  await database.Connect(connector, new PowerSyncConnectionOptions(
    crudUploadThrottleMs: 500
  ));
  ```
</CodeGroup>

#### Error Handling

<Warning>
  If your `uploadData()` throws an error (e.g. due to a `4xx` or `5xx` response from your backend), the SDK will **retry** the same upload indefinitely, effectively blocking the upload queue. Your backend should return `2xx` for validation errors or write conflicts, and reserve error responses for transient failures. See [Writing Client Changes](/handling-writes/writing-client-changes#recommendations) and [Handling Write / Validation Errors](/handling-writes/handling-write-validation-errors) for recommended patterns.
</Warning>

When `uploadData()` throws an error:

1. The SDK logs the error and updates the sync status with an `uploadError`.
2. It waits for the retry delay (default: 5 seconds).
3. It retries the upload from the beginning of the queue.
4. If the sync stream disconnects during the retry wait, the upload loop exits and will resume when the connection is re-established.

#### Stalled Upload Queue Detection

If the SDK detects that the same CRUD entry is at the front of the queue across consecutive upload iterations (i.e., `uploadData()` returned without error but didn't call `.complete()` on the batch or transaction), it logs a warning:

> *"Potentially previously uploaded CRUD entries are still present in the upload queue. Make sure to handle uploads and complete CRUD transactions or batches by calling and awaiting their \[.complete()] method. The next upload iteration will be delayed."*

After logging this warning, the SDK **throws an internal error**, which causes it to enter the retry delay path (default: 5 seconds) before attempting the upload again. This prevents a tight loop when `uploadData()` consistently fails to process entries.

This typically means your `uploadData()` implementation is not calling `.complete()` after successfully processing entries. Always call `.complete()` on the `CrudBatch` or `CrudTransaction` to remove processed entries from the queue.

<Tip>
  `uploadData()` is only called while the sync stream is connected. If the device is offline, writes accumulate in the upload queue and are uploaded automatically when connectivity is restored and the sync stream reconnects.
</Tip>

### When `fetchCredentials()` is Called

The PowerSync Client SDK **caches credentials internally** and `fetchCredentials` is called in the following scenarios:

1. **On initial connection** - When `connect()` is called and no cached credentials are available.
2. **Before the token expires** - The PowerSync Service sends periodic keepalive messages that include the remaining lifetime of the current JWT. When the token has **30 seconds or less** remaining, the SDK pre-fetches new credentials in the background to ensure a seamless transition.
3. **When the token has expired** - If the token expires (e.g. due to the device being offline), the SDK invalidates cached credentials and calls `fetchCredentials()` again when reconnecting.
4. **On authentication errors** - If the PowerSync Service responds with a `401` status, credentials are invalidated, prompting a fresh `fetchCredentials()` call on the next connection attempt.

For a JWT with a 1-hour expiry, `fetchCredentials()` is typically called **approximately once per hour**.

<Tip>
  Your `fetchCredentials()` implementation should always return a fresh token when called, even if the currently cached token is not yet expired.
</Tip>

### Recommended JWT Expiry Duration

A JWT expiry (TTL) of **5 to 60 minutes** works well with PowerSync:

* **Shorter JWTs (5–15 minutes):** More secure with minimal overhead, since `fetchCredentials()` is only called when the token is near expiry.
* **Longer JWTs (30–60 minutes):** Fewer credential fetches and simpler backend implementation.

The SDK handles token rotation seamlessly regardless of the TTL, as long as `fetchCredentials()` can return a valid token when called.

Avoid JWTs with a TTL shorter than 30 seconds, since the SDK's pre-fetch threshold is 30 seconds - shorter TTLs would bypass pre-fetching and always hit the token-expired reconnection path.

### Token Expiry While Offline

If the JWT expires while the device is offline, the SDK handles reconnection automatically - it will call `fetchCredentials()` to obtain a new token when connectivity is restored.

The PowerSync sync token is used exclusively for the connection between the Client SDK and the PowerSync Service. It is separate from your application's own authentication session (e.g. Supabase Auth, Firebase Auth), which has its own lifecycle and refresh mechanism.

## Example Implementation

For an example implementation of a PowerSync 'backend connector', see the SDK guide for your platform:

<CardGroup cols={3}>
  <Card title="Dart/Flutter" icon="flutter" href="/client-sdks/reference/flutter#3-integrate-with-your-backend" />

  <Card title="React Native & Expo" icon="react" href="/client-sdks/reference/react-native-and-expo#3-integrate-with-your-backend" />

  <Card title="JavaScript Web" icon="js" href="/client-sdks/reference/javascript-web#3-integrate-with-your-backend" />

  <Card title="Capacitor (alpha)" icon="c" href="/client-sdks/reference/capacitor#3-integrate-with-your-backend" />

  <Card title="Node.js (beta)" icon="node-js" href="/client-sdks/reference/node#3-integrate-with-your-backend" />

  <Card title="Tauri (alpha)" icon="https://mintcdn.com/powersync/65BugQOyCcrBb77m/logo/tauri_sidebar.svg?fit=max&auto=format&n=65BugQOyCcrBb77m&q=85&s=a428d1e5f882e76f44575741ba6bc4d7" href="/client-sdks/reference/tauri#3-integrate-with-your-backend" width="128" height="128" data-path="logo/tauri_sidebar.svg" />

  <Card title="Kotlin" icon="android" href="/client-sdks/reference/kotlin#3-integrate-with-your-backend" />

  <Card title="Swift" icon="swift" href="/client-sdks/reference/swift#3-integrate-with-your-backend" />

  <Card title=".NET (beta)" icon="microsoft" href="/client-sdks/reference/dotnet#3-integrate-with-your-backend" />

  <Card title="Rust (alpha)" icon="rust" href="/client-sdks/reference/rust#3-integrate-with-your-backend" />
</CardGroup>

## More Examples

For additional implementation examples, see the [Examples](/intro/examples) section.
