> ## Documentation Index
> Fetch the complete documentation index at: https://powersync-sync-streams-nav.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# JavaScript Web SDK

> Build JavaScript web apps that use client-side SQLite for real-time updates across users and devices, instant interactions and continued functionality when offline.

The PowerSync JavaScript Web SDK runs SQLite in the browser and keeps it in sync with your backend database. Your UI reads and writes local data directly, so interactions feel instant because they do not wait on network round trips. Watch queries emit new results as local or synced data changes, keeping the UI current without polling.

With Sync Streams, you define the subset of backend data each client receives rather than replicating the entire database into every browser. This partial sync reduces data transfer and local storage use by keeping unrelated data out of the client database. Reading from a reactive local database also reduces the need for per-screen read APIs, cache invalidation, and client-side state plumbing. Synced data remains available even when connectivity is interrupted, enabling offline-first PWAs.

```text Build with AI icon="sparkles" wrap theme={null}
Install the PowerSync Agent Skills with: npx skills add powersync-ja/agent-skills. Then follow the skills to onboard this project to PowerSync using the JavaScript Web SDK.
```

<CardGroup cols={3}>
  <Card title="PowerSync SDK on NPM" icon="npm" href="https://www.npmjs.com/package/@powersync/web">
    This SDK is distributed via NPM
  </Card>

  <Card title="Source Code" icon="github" href="https://github.com/powersync-ja/powersync-js/tree/main/packages/web">
    Refer to packages/web in the `powersync-js` repo on GitHub
  </Card>

  <Card title="API Reference" icon="book" href="https://powersync-ja.github.io/powersync-js/web-sdk">
    Full API reference for the SDK
  </Card>

  <Card title="Example Projects" icon="code" href="/intro/examples">
    Gallery of example projects/demo apps built with JavaScript Web stacks and PowerSync
  </Card>

  <Card title="Changelog" icon="megaphone" href="https://releases.powersync.com/announcements/powersync-js-web-client-sdk">
    Changelog for the SDK
  </Card>
</CardGroup>

## SDK Features

* **Real-time streaming of database changes**: Changes made by one user are instantly streamed to all other users with access to that data. This keeps clients automatically in sync without manual polling or refresh logic.
* **Direct access to a local SQLite database**: Data is stored locally, so apps can read and write instantly without network calls. This enables offline support and faster user interactions.
* **Asynchronous background execution**: The SDK performs database operations in the background to avoid blocking the application’s main thread. This means that apps stay responsive, even during heavy data activity.
* **Query subscriptions for live updates**: The SDK supports query subscriptions that automatically push real-time updates to client applications as data changes, keeping your UI reactive and up to date.
* **Automatic schema management**: PowerSync syncs schemaless data and applies a client-defined schema using SQLite views. This architecture means that PowerSync SDKs handle schema changes without explicit migrations on the client side.

## Single-Page Application (SPA) Frameworks

The PowerSync JavaScript Web SDK is compatible with popular Single-Page Application (SPA) frameworks like React, Vue, Angular, and Svelte. Integration packages are provided specifically for the following:

<CardGroup>
  <Card title="React Hooks" icon="react" href="/client-sdks/frameworks/react" horizontal>
    Wrapper package to support reactivity and live queries.
  </Card>

  <Card title="Vue Composables" icon="vuejs" href="/client-sdks/frameworks/vue" horizontal>
    Wrapper package to support reactivity and live queries.
  </Card>

  <Card title="TanStack Query & DB" icon="tree-palm" href="/client-sdks/frameworks/tanstack" horizontal>
    PowerSync integrates with TanStack Query and TanStack DB for reactive data management.
  </Card>

  <Card title="Nuxt Module" icon={<svg xmlns="http://www.w3.org/2000/svg" width={24} height={24} viewBox="0 0 24 24" fill="none" stroke="#4e89ff" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}><path stroke="none" d="M0 0h24v24H0z" /><path d="m12.146 8.583-1.3-2.09a1.046 1.046 0 0 0-1.786.017l-5.91 9.908A1.046 1.046 0 0 0 4.047 18H7.96M20.043 18c.743 0 1.201-.843.82-1.505l-4.044-7.013a.936.936 0 0 0-1.638 0l-4.043 7.013c-.382.662.076 1.505.819 1.505h8.086" /></svg>} href="/client-sdks/frameworks/nuxt" horizontal>
    PowerSync Nuxt module to build offline/local first apps using Nuxt.
  </Card>
</CardGroup>

<Accordion title="Which package should I choose for queries?">
  For React or React Native apps:

  * The [`@powersync/react`](/client-sdks/frameworks/react) package is best for most basic use cases, especially when you only need reactive queries with loading and error states.

  * For more advanced scenarios, such as query caching and pagination, use [TanStack Query](/client-sdks/frameworks/tanstack#tanstack-query). The [`@powersync/tanstack-react-query`](/client-sdks/frameworks/tanstack#tanstack-query) package extends the `useQuery` hook from `@powersync/react` with functionality from [TanStack Query](https://tanstack.com/query/latest/docs/framework/react/overview).

  * For reactive data management and live query support across multiple frameworks, consider [TanStack DB](/client-sdks/frameworks/tanstack#tanstack-db). PowerSync works with all TanStack DB framework adapters (React, Vue, Solid, Svelte, Angular).

  If you have a Vue app, use the Vue-specific package: [`@powersync/vue`](/client-sdks/frameworks/vue).
</Accordion>

## Installation

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>

## Getting Started

**Prerequisites:** Before you start, connect your source database to the PowerSync Service and deploy Sync Streams. These are steps 1-4 in the [Setup Guide](/intro/setup-guide).

### 1. Define the Client-Side Schema

The client-side schema defines the tables and columns of the SQLite database that the PowerSync client SDK manages and that your app reads from and writes to. It is usually derived from your backend database schema and your [Sync Streams](/sync/streams/overview), and it can also include [local-only tables](/client-sdks/advanced/local-only-usage). You apply the schema when you instantiate the database in the next step.

Schema migrations are not required. The SDK syncs schemaless data and applies the schema to that data with SQLite views. The exception is [raw tables](/client-sdks/advanced/raw-tables), which you create and migrate yourself.

<Tip>
  **Generate schema automatically**

  In the [PowerSync Dashboard](https://dashboard.powersync.com/), select your project and instance and click the **Connect** button in the top bar to generate the client-side schema in your preferred language. The schema is generated from your Sync Streams. The [CLI](/tools/cli) offers the same function.

  The generated schema does not include an `id` column. The client SDK creates an `id` column of type `text` automatically, so you do not need to declare it. See [Client ID](/sync/advanced/client-id) for details.
</Tip>

The available column types are `text`, `integer`, and `real`. These should match the values produced by your Sync Streams. If a value does not match, it is cast automatically. For details on how source database types map to SQLite types, see [Types](/sync/types).

**Example:**

```js theme={null}
// AppSchema.ts
import { column, Schema, Table } from '@powersync/web';

const lists = new Table({
  created_at: column.text,
  name: column.text,
  owner_id: column.text
});

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,
  lists
});

// For types
export type Database = (typeof AppSchema)['types'];
export type TodoRecord = Database['todos'];
// OR:
// export type Todo = RowType<typeof todos>;
export type ListRecord = Database['lists'];
```

<Note>
  You do not need to declare an `id` column. PowerSync creates it automatically.
</Note>

### 2. Instantiate the PowerSync Database

Next, instantiate the PowerSync database. PowerSync streams changes from your backend source database into the client-side SQLite database, based on your Sync Streams. Your app reads from and writes to this local database whether the user is online or offline.

**Example:**

```js theme={null}
import { PowerSyncDatabase } from '@powersync/web';
import { Connector } from './Connector';
import { AppSchema } from './AppSchema';

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'
    // Optional. Directory where the database file is located.
    // dbLocation: 'path/to/directory'
  }
});
```

After you instantiate the PowerSync database, call the [connect()](https://powersync-ja.github.io/powersync-js/common/interfaces/CommonPowerSyncDatabase#connect) method to sync data with your backend.

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

```js theme={null}
export const setupPowerSync = async () => {
  // Uses the backend connector that you create in the next step
  const connector = new Connector();
  db.connect(connector);
};
```

### 3. Integrate with Your Backend

The backend connector connects the PowerSync client SDK to your application backend. The SDK uses it to:

1. Get an auth token to connect to the PowerSync instance.
2. Upload client-side writes to your backend API. The SDK places every write to the SQLite database in an upload queue and uploads the queue to your backend when the user is connected. Your backend then applies the changes to the source database.

The connector must implement two methods:

1. [PowerSyncBackendConnector.fetchCredentials](https://github.com/powersync-ja/powersync-js/blob/ed5bb49b5a1dc579050304fab847feb8d09b45c7/packages/common/src/client/connection/PowerSyncBackendConnector.ts#L16) - The SDK calls this method to get authentication credentials. It caches the credentials and calls the method again only when needed, for example on the first connection or when the token is near expiry. See [When `fetchCredentials()` is Called](/configuration/app-backend/client-side-integration#when-fetchcredentials-is-called) for details and [Authentication Setup](/configuration/auth/overview) for how to generate credentials.
2. [PowerSyncBackendConnector.uploadData](https://github.com/powersync-ja/powersync-js/blob/ed5bb49b5a1dc579050304fab847feb8d09b45c7/packages/common/src/client/connection/PowerSyncBackendConnector.ts#L24) - The SDK calls this method whenever it has client-side writes to upload to your backend API. Implement how those writes are processed and uploaded. See [When `uploadData()` is Called](/configuration/app-backend/client-side-integration#when-uploaddata-is-called) for triggers, throttling, and retry behavior, and [Writing Client Changes](/handling-writes/writing-client-changes) for the app backend implementation.

**Example:**

```js theme={null}
import { UpdateType } from '@powersync/web';

export class Connector {
  async fetchCredentials() {
    // Implement fetchCredentials to obtain a JWT from your authentication service. 
    // See https://docs.powersync.com/configuration/auth/overview
    return {
        endpoint: '[Your PowerSync instance URL or self-hosted endpoint]',
        // Use a development token (see Authentication Setup https://docs.powersync.com/configuration/auth/development-tokens) to get up and running quickly
        token: 'An authentication token'
    };
  }

  async uploadData(database) {
    // Implement uploadData to send local changes to your backend service.
    // You can omit this method if you only want to sync data from the database to the client

    // See example implementation here: https://docs.powersync.com/client-sdks/usage-examples#send-changes-in-local-data-to-your-backend-service
  }
}
```

### 4. Subscribe to Sync Streams

Streams defined with `auto_subscribe: true` start syncing as soon as the client connects. For all other streams, your app must subscribe before their data downloads. The basic pattern is: subscribe to a stream, wait for its data to sync, then unsubscribe when the data is no longer needed.

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

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

// The stream's rows are now in the local SQLite database.
// TODO: Read the todos for this list with a local query.

// When the data is no longer needed
sub.unsubscribe();
```

If you use React, the `useQuery` hook accepts a `streams` option and the `useSyncStream` hook manages a subscription for you. See [Framework Integrations](/sync/streams/client-usage#framework-integrations).

After you unsubscribe, the synced data stays in the local database for the stream's time-to-live (TTL), which is 24 hours by default. If the app subscribes again within that time, the data is already available. See [Client-Side Usage](/sync/streams/client-usage) for framework hooks, per-subscription sync status, custom TTLs, priority overrides, and connection parameters.

## Using PowerSync: CRUD Functions

Once the PowerSync database is connected and your streams have synced, the data is in the local SQLite database.

The most commonly used CRUD functions to interact with your SQLite data are:

* [PowerSyncDatabase.get](/client-sdks/reference/javascript-web#fetching-a-single-item) - get (`SELECT`) a single row from a table.
* [PowerSyncDatabase.getAll](/client-sdks/reference/javascript-web#querying-items-powersync-getall) - get (`SELECT`) a set of rows from a table.
* [PowerSyncDatabase.watch](/client-sdks/reference/javascript-web#watching-queries-powersync-watch) - execute a read query every time a dependent table changes.
* [PowerSyncDatabase.execute](/client-sdks/reference/javascript-web#mutations-powersync-execute-powersync-writetransaction) - execute a write (`INSERT`/`UPDATE`/`DELETE`) query.

### Fetching a Single Item

The [get](https://powersync-ja.github.io/powersync-js/common/interfaces/CommonPowerSyncDatabase#get) method executes a read-only (SELECT) query and returns a single result. It throws an exception if no result is found. Use [getOptional](https://powersync-ja.github.io/powersync-js/common/interfaces/CommonPowerSyncDatabase#getoptional) to return a single optional result (returns `null` if no result is found).

```js theme={null}
// Find a list item by ID
export const findList = async (id) => {
  const result = await db.get('SELECT * FROM lists WHERE id = ?', [id]);
  return result;
}
```

### Querying Items (PowerSync.getAll)

The [getAll](https://powersync-ja.github.io/powersync-js/common/interfaces/CommonPowerSyncDatabase#getall) method returns a set of rows from a table.

```js theme={null}
// Get all lists
export const getLists = async () => {
  const results = await db.getAll('SELECT * FROM lists');
  return results;
}
```

### Watching Queries (PowerSync.watch)

The [watch](https://powersync-ja.github.io/powersync-js/common/interfaces/CommonPowerSyncDatabase#watch) method executes a read query whenever a change to a dependent table is made.

<Tabs>
  <Tab title="AsyncIterator approach">
    ```javascript theme={null}
    async function* pendingLists(): AsyncIterable<string[]> {
      for await (const result of db.watch(
        `SELECT * FROM lists WHERE state = ?`,
        ['pending']
      )) {
        yield result.rows?._array ?? [];
      }
    }
    ```
  </Tab>

  <Tab title="Callback approach">
    ```javascript theme={null}
    const pendingLists = (onResult: (lists: any[]) => void): void => {
      db.watch(
        'SELECT * FROM lists WHERE state = ?',
        ['pending'],
        {
          onResult: (result: any) => {
            onResult(result.rows?._array ?? []);
          }
        }
      );
    }
    ```
  </Tab>
</Tabs>

For advanced watch query features such as incremental updates and differential results, see [Live Queries / Watch Queries](/client-sdks/watch-queries).

### Mutations (PowerSync.execute, PowerSync.writeTransaction)

The [execute](https://powersync-ja.github.io/powersync-js/common/interfaces/CommonPowerSyncDatabase#execute) method can be used for executing single SQLite write statements.

```js theme={null}
// Delete a list by ID
export const deleteList = async (id) => {
  await db.execute('DELETE FROM lists WHERE id = ?', [id]);
};

// OR: delete the list and its todos in one transaction
export const deleteListWithTodos = async (id) => {
  await db.writeTransaction(async (tx) => {
    await tx.execute('DELETE FROM todos WHERE list_id = ?', [id]);
    await tx.execute('DELETE FROM lists WHERE id = ?', [id]);
  });
};
```

<Note>
  When using the default client-side [JSON-based view system](/architecture/client-architecture#client-side-schema-and-sqlite-database-structure), writes are applied to a view, with triggers writing to the underlying table. Because of this, `result.rowsAffected` from `db.execute()` can be `0` even when an `UPDATE` or `DELETE` succeeds.

  When you need to confirm whether a mutation changed any rows, add a `RETURNING` clause and check the returned rows:

  ```js theme={null}
  const result = await db.execute(
    'UPDATE tasks SET deleted_at = ? WHERE id = ? AND deleted_at IS NULL RETURNING id',
    [now, id]
  );

  const wasUpdated = (result.rows?.length ?? 0) > 0;
  ```

  If you need direct table writes, use [raw tables](/client-sdks/advanced/raw-tables).
</Note>

## Configure Logging

```js theme={null}
import { createConsoleLogger, LogLevels } from '@powersync/web';

// Create a logger with trace minimum level to see all log messages
// Available levels: trace, debug, info, warn, error
const logger = createConsoleLogger({ minLevel: LogLevels.trace });
```

<Tip>
  Enable verbose output in the developer tools for detailed logs.
  Note that the PowerSync SDK relies on web workers, which inherit the logger from the main database while using a separate log level.
  To pass a consistent log level to workers, also pass it to the sync and database workers:

  ```TypeScript theme={null}
  const db = new PowerSyncDatabase({
    database: {
      dbFilename: '...',
      databaseWorkerLogLevel: LogLevels.trace,
    },
    sync: {
      logLevel: LogLevels.trace,
    }
  });
  ```
</Tip>

Additionally, the [WASQLiteOpenFactory](https://powersync-ja.github.io/powersync-js/web-sdk/classes/WASQLiteOpenFactory) opens SQLite connections inside a shared web worker. This worker can be inspected in Chrome by accessing:

```
chrome://inspect/#workers
```

## Additional Usage Examples

For more usage examples including accessing connection status, monitoring sync progress, and waiting for initial sync, see the [Usage Examples](/client-sdks/usage-examples) page.

## ORM Support

See [JavaScript ORM Support](/client-sdks/orms/js/overview) for details.

## Vite Quickstart Tutorial

<iframe className="w-full aspect-video rounded-xl" src="https://www.youtube.com/embed/wAMIeuVxHd0?si=yevqTB7Of4nEYHKT&cc_load_policy=1" title="Quickly set up a PowerSync app using our Vite template" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen />

Template repo: [vite-react-ts-powersync-supabase](https://github.com/powersync-community/vite-react-ts-powersync-supabase/)

## Troubleshooting

See [Troubleshooting](/debugging/troubleshooting) for pointers to debug common issues.

## Supported Platforms

See [Supported Platforms -> JS/Web SDK](/resources/supported-platforms#javascript).

## Upgrading the SDK

Run the following command in your project folder:

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

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

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

## Developer Notes

### Connection Methods

This SDK supports two methods for streaming sync commands:

1. **HTTP Streaming (Default)**
   * This is the default and recommended connection method.
2. **WebSocket**
   * This implementation uses RSocket over WebSocket connections.
   * To customize window sizes for flow control and back-pressure, set `fetchStrategy` to `Buffered` (default) or `Sequential`.
   * On the web, there is no compelling reason to use WebSockets over HTTP response streams.

By default, the `PowerSyncDatabase.connect()` method uses HTTP streaming. You can optionally specify the `connectionMethod` to override this:

```js theme={null}
// HTTP Streaming (default)
powerSync.connect(connector);

// WebSocket
powerSync.connect(connector, { connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET });
```

### SQLite Virtual File Systems

This SDK supports multiple Virtual File Systems (VFS), each responsible for storing the local SQLite database. The VFS you choose determines where data persists, how multiple browser tabs interact with the database, and whether concurrent reads are possible.

#### 1. IDBBatchAtomicVFS (Default)

The default VFS for applications that need the broadest browser compatibility. It uses IndexedDB for storage. Multiple tabs are fully supported across most modern browsers, and no additional configuration is needed.

#### 2. OPFS-Based Alternatives

PowerSync supports three OPFS (Origin Private File System) implementations that are generally faster than IndexedDB:

**OPFSCoopSyncVFS**

Recommended for applications requiring multi-tab support, especially on Safari/iOS. This implementation provides multi-tab support across all major browsers and offers the most reliable compatibility with Safari and Safari iOS.

Example configuration:

```js theme={null}
import { PowerSyncDatabase, WASQLiteVFS } from '@powersync/web';

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

**AccessHandlePoolVFS**

Use this for single-tab applications that want a straightforward OPFS setup with one worker and direct use of the OPFS Access Handle API. It is not designed for multiple tab use cases.

Example configuration:

```js theme={null}
import { PowerSyncDatabase, WASQLiteVFS } from '@powersync/web';

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

**OPFSWriteAheadVFS**

Use this for read-heavy applications that benefit from parallel reads. This VFS uses SQLite's write-ahead log (WAL) mode and opens multiple connections so reads can run concurrently.

This VFS is only supported on Chromium-based browsers (for example Chrome or Edge). Safari, Firefox, and related environments are not supported.

The `additionalReaders` option (defaults to `1`) controls how many extra read-only database connections PowerSync opens alongside the primary connection. Increase it when you routinely run many read queries at once; each extra reader uses more memory. The `useWebWorker` flag must not be set to `false`, because this setup relies on web workers.

Example configuration with 2 additional readers (3 total concurrent reads):

```js theme={null}
import { PowerSyncDatabase, WASQLiteVFS } from '@powersync/web';

export const db = new PowerSyncDatabase({
  schema: AppSchema,
  database: {
    dbFilename: 'exampleVFS.db',
    vfs: WASQLiteVFS.OPFSWriteAheadVFS,
    additionalReaders: 2
  }
});
```

#### 3. In-Memory VFS

Since version 1.39.0 of the `@powersync/web` package, you can use an in-memory database with `WASQLiteVFS.InMemoryVfs`. It runs queries faster than any other single-threaded VFS (both IndexedDB and OPFS, except the write-ahead VFS).

No data is persisted: local writes are lost if they aren't uploaded before the tab is closed, and all data is resynced whenever
a tab is opened. This makes it unsuitable for apps that need to work offline, but a good fit for:

* Development, where starting from a fresh database on every load makes it easy to reproduce issues from a clean state.
* Online-only apps with very frequent queries and small datasets.

<Warning>
  When using in-memory databases in production scenarios, consider watching `SELECT * FROM ps_crud LIMIT 1` to detect whether outstanding
  local mutations exist and indicate that state to the user. A `beforeunload` event listener can also be useful in this state to
  call `preventDefault()` on tab close events, causing browsers to ask for confirmation before closing the tab.
</Warning>

With Chrome and Firefox on desktop, this VFS uses a shared worker to enable multi-tab support by default, meaning that all
tabs have access to the same data and will share a sync worker.
This behavior can be enabled or disabled on all browsers by passing the [`enableMultiTabs` flag](#available-flags).

If support for multi-tabs is not desired, consider giving each tab a uniquely-named PowerSync instance. The in-memory database
would not be shared across tabs in either case, but only one PowerSync database with the same name can sync at a time.
Unique names ensure databases across tabs are fully independent:

```js theme={null}
export const db = new PowerSyncDatabase({
  schema: AppSchema,
  database: {
    dbFilename: `memory-${crypto.randomUUID()}.db`,
    vfs: WASQLiteVFS.InMemoryVfs,
    enableMultiTabs: false
  }
});
```

#### VFS / Option Compatibility Matrix

| VFS Type / Option         | Multi-Tab (Standard) | Multi-Tab (Safari/iOS) | Concurrent Reads | Best For                                                                     |
| ------------------------- | -------------------- | ---------------------- | ---------------- | ---------------------------------------------------------------------------- |
| IDBBatchAtomicVFS         | ✅                    | ❌                      | ❌                | Broadest compatibility, minimal setup                                        |
| OPFSCoopSyncVFS           | ✅                    | ✅                      | ❌                | Multi-tab + Safari/iOS support                                               |
| AccessHandlePoolVFS       | ❌                    | ❌                      | ❌                | Single-tab, single worker, access-handle OPFS                                |
| OPFSWriteAheadVFS         | ✅                    | ❌                      | ✅                | Chromium only; parallel reads via WAL + readers                              |
| InMemoryVfs               | ✅                    | ❌                      | ❌                | Development, small data sizes                                                |
| InMemoryWriteAheadLogPool | ❌ (isolated per tab) | ❌ (isolated per tab)   | ✅                | Highly concurrent, non-persistent workloads; requires cross-origin isolation |

<Note>
  There are known issues with OPFS (all variants) in Safari's incognito mode.
</Note>

### Multi-Threaded In-Memory SQLite Connection Pool

Since version 2.2.0, the `@powersync/web` package provides an experimental, per-tab in-memory SQLite connection pool for highly concurrent query workloads. It uses a design inspired from `OPFSWriteAheadVFS`, but relies on `SharedArrayBuffer` to coordinate an in-memory database instead of persisting to OPFS. It is available under a separate import and is thus configured via the `opened` option on `PowerSyncDatabase` instead of `database.vfs`.

<Note>This setup is experimental and might change in the future.</Note>

The pool creates `numWorkers` SQLite connections in dedicated web workers. One worker is the designated writer, while the remaining workers are used as additional readers. The writer can also serve reads while it is idle.

Read-only transactions can execute in parallel, including while the writer appends changes to a custom in-memory write-ahead overlay. The database and write-ahead-log buffers are backed by growable `SharedArrayBuffer` objects shared across the workers. This parallelism benefits workloads with overlapping queries. It does not inherently make an individual sequential query faster, and additional workers increase startup time and memory usage.

Each pool instance is independent and belongs to one tab. Its data is not persisted or shared across tabs, and the application cannot assign it a database filename. Opening or refreshing a tab creates a fresh database that must be resynced. Any local writes that have not been uploaded are lost when the tab closes.

This option requires cross-origin isolation and browser support for growable `SharedArrayBuffer`.

This connection pool is primarily relevant when all of the following apply:

1. You need highly concurrent, high-performance queries in your app.
2. At the same time, the overall database size (or at least the actively synced part of the database) is relatively small, as it gets
   synced every time a tab is opened.
3. You don't need persistence.
4. You can enable [cross-origin isolation](https://web.dev/articles/cross-origin-isolation-guide) by using the appropriate headers.
   Without cross-origin isolation and shared array buffers, constructing the pool will throw.
5. You don't need multiple tabs to share offline state.

The pool is exposed through a separate package entry point so applications that do not use it do not include it in their main bundle:

```js theme={null}
import { InMemoryWriteAheadLogPool } from '@powersync/web/extra/shared-memory-pool';
import { PowerSyncDatabase } from '@powersync/web';

export const db = new PowerSyncDatabase({
  schema: AppSchema,
  opened: new InMemoryWriteAheadLogPool({
    numWorkers: 3 // Uses one writer, two additional workers for reads.
  }),
});
```

### Managing OPFS Storage

Unlike IndexedDB, OPFS storage cannot be managed through browser developer tools. The following utility functions can help you manage OPFS storage programmatically:

```js theme={null}
// Clear all OPFS storage
async function purgeVFS() {
  await powerSync.disconnect();
  await powerSync.close();

  const root = await navigator.storage.getDirectory();
  await new Promise(resolve => setTimeout(resolve, 1)); // Allow .db-wal to become deletable

  for await (const [name, entry] of root.entries!()) {
    try {
      if (entry.kind === 'file') {
        await root.removeEntry(name);
      } else if (entry.kind === 'directory') {
        await root.removeEntry(name, { recursive: true });
      }
    } catch (err) {
      console.error(`Failed to delete ${entry.kind}: ${name}`, err);
    }
  }
}

// List OPFS entries
async function listVfsEntries() {
  const root = await navigator.storage.getDirectory();
  for await (const [name, entry] of root.entries()) {
    console.log(`${entry.kind}: ${name}`);
  }
}
```

### Multiple Tab Support

<Warning>
  * Full multi-tab support relies on shared web workers, which are disabled by default on Android, iOS, and Safari. On these platforms, the SDK falls back to a less reliable broadcast-based mechanism, as described below.
  * For Safari, use the [`OPFSCoopSyncVFS`](/client-sdks/reference/javascript-web#sqlite-virtual-file-systems) virtual file system to ensure stable multi-tab functionality.
</Warning>

Using PowerSync between multiple tabs is supported on most desktop browsers. Multiple tab support relies on shared web workers for database and sync operations. When enabled, the SDK creates a shared web worker named `shared-powersync-[dbFileName]`.

The shared sync worker connects to the PowerSync Service and applies changes to the database on behalf of all tabs. It calls the `fetchCredentials` and `uploadData` methods of the most recently opened tab. When that tab closes, the worker uses the previously opened tab instead. When using an IndexedDB-based VFS, the SDK can also open database connections in a shared worker so that writes made in one tab are instantly available in the others.

Multi-tab support is enabled by default where available. You can disable it with the [`enableMultiTabs` flag](#available-flags):

```js theme={null}
export const db = new PowerSyncDatabase({
  schema: AppSchema,
  database: {
    dbFilename: 'my_app_db.sqlite',
    enableMultiTabs: false
  },
});
```

#### Behavior Without Shared Workers

When multi-tab support is disabled, whether explicitly or because the platform does not support it, each tab spawns a standard web worker for database operations. These workers can safely operate on the database concurrently. Only one tab connects and syncs at a time, and only that tab's `fetchCredentials` and `uploadData` methods are called.

The SDK still tries to share state across tabs using broadcast channels (since version 2.1.0 of the SDK): update notifications for watched queries, the sync status (fields like `hasSynced` and download progress), and sync stream subscriptions made in any tab. This is less reliable than shared workers, so updates may not reach every tab.

### Using PowerSyncDatabase Flags

The `PowerSyncDatabase` constructor accepts the following flags. Use them to enable or disable specific features.

#### Configuring Options

You can configure these options during the initialization of `PowerSyncDatabase` as top-level constructor properties.

```javascript theme={null}
import { PowerSyncDatabase } from '@powersync/web';
import { AppSchema } from '@/library/powersync/AppSchema';

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

#### Available Flags

<ParamField path="database.enableMultiTabs">
  default: `true` (`false` on Android, iOS, and Safari)

  Enables support for multiple tabs using shared web workers. When enabled, multiple tabs share the same database and sync connection.
</ParamField>

<ParamField path="broadcastLogs">
  default: `true`

  Enables the broadcasting of logs for debugging purposes. This flag helps monitor shared worker logs in a multi-tab environment.
</ParamField>

<ParamField path="database.disableSSRWarning">
  default: `false`

  Disables warnings when running in SSR (Server-Side Rendering) mode.
</ParamField>

<ParamField path="database.ssrMode">
  default: `false`

  Enables SSR mode. In this mode, only empty query results will be returned, and syncing with the backend is disabled.
</ParamField>

<ParamField path="database.useWebWorker">
  default: `true`

  Enables the use of web workers for database operations. Disabling this flag also disables multi-tab support.
</ParamField>

#### Flag Behavior

**Example 1: Multi-Tab Support**

By default, multi-tab support is enabled if supported by the browser. To explicitly disable this feature:

```javascript theme={null}
export const db = new PowerSyncDatabase({
  schema: AppSchema,
  database: {
    dbFilename: 'my_app_db.sqlite',
    enableMultiTabs: false,
  },
});
```

When disabled, each tab uses independent workers. The SDK tries to share sync status and update notifications for watched queries across tabs, but this is less reliable than shared workers. See [Behavior Without Shared Workers](#behavior-without-shared-workers).

**Example 2: Verbose Debugging with Broadcast Logs**

To enable detailed logging for debugging:

```javascript theme={null}
export const db = new PowerSyncDatabase({
  schema: AppSchema,
  database: {
    dbFilename: 'my_app_db.sqlite',
    databaseWorkerLogLevel: LogLevels.debug,
  },
  sync: {
    logLevel: LogLevels.debug,
  },
  logger: createConsoleLogger({ minLevel: LogLevels.debug }),
});
```

Logs include details of database and sync operations.

#### Recommendations

1. **Set `enableMultiTabs`** to `true` if your application shares data across multiple tabs.
2. **Set `broadcastLogs`** to `true` during development to troubleshoot and monitor database and sync operations.
