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

# JavaScript Web SDK: VFS and Multi-Tab Support

> Choose browser storage and configure how tabs share a local database and sync connection in the JavaScript Web SDK.

## SQLite Virtual File Systems

The SDK stores its local SQLite database through a virtual file system (VFS), the layer SQLite uses to read and write files. The VFS determines whether data persists, how tabs share data, and whether reads can run concurrently.

For new apps that need to support a wide range of browsers, use [`vfs: 'auto'`](#automatic-vfs-selection-recommended). It selects [`SimpleOpfsVFS`](#simpleopfsvfs), which supports multiple tabs across major browsers, when the Origin Private File System (OPFS) is available. If OPFS is unavailable, or you disable web workers, it uses an in-memory database that does not persist data. If your app primarily targets Safari, use [`OPFSCoopSyncVFS`](#opfscoopsyncvfs) for now.

If you omit `vfs`, the SDK uses [`IDBBatchAtomicVFS`](#idbbatchatomicvfs-default) and stores data in IndexedDB. This remains the default for backward compatibility.

### VFS Comparison

On smaller screens, scroll the table horizontally.

| Option | Storage | Multi-Tab (Standard) | Multi-Tab (Safari/iOS) | Concurrent Reads | Recommendation |
| - | - | - | - | - | - |
| [SimpleOpfsVFS](#simpleopfsvfs) | OPFS | ✅ | ✅ | ❌ | Selected by `'auto'` for broad browser support; use `OPFSCoopSyncVFS` if Safari is your primary target |
| [IDBBatchAtomicVFS](#idbbatchatomicvfs-default) | IndexedDB | ✅ | ❌ | ❌ | Default for backward compatibility; use when persistence is required without web workers |
| [OPFSCoopSyncVFS](#opfscoopsyncvfs) | OPFS | ✅ | ✅ | ❌ | Use if Safari is your primary target |
| [OPFSWriteAheadVFS](#opfswriteaheadvfs) | OPFS | ✅ | ❌ | ✅ | Apps that need concurrent reads and only target Chromium-based browsers |
| [AccessHandlePoolVFS](#accesshandlepoolvfs) | OPFS | ❌ | ❌ | ❌ | Backward compatibility only; do not select for new apps |
| [InMemoryVfs](#inmemoryvfs) | Memory | ✅ | ❌ | ❌ | Development or online-only apps with small datasets; avoids OPFS and IndexedDB overhead, without persistence |
| [InMemoryWriteAheadLogPool](#multi-threaded-in-memory-sqlite-connection-pool) | Memory | ❌ (isolated per tab) | ❌ (isolated per tab) | ✅ | Concurrent reads without persistence; experimental and requires cross-origin isolation |

> [!NOTE]
>
> In this table, **Multi-Tab** means that tabs can access the same local database. It does not mean that they share a sync connection. The `enableMultiTabs` option separately controls whether the SDK uses shared workers for sync and, for some VFS options, database access. See [Multiple Tab Support](#multiple-tab-support).

All OPFS options require `useWebWorker: true` (which is the default). OPFS access [can fail in private browsing](https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/getDirectory#exceptions).

> [!WARNING]
>
> Before switching VFS implementations, upload pending mutations. Use a new database file name if the switch changes the storage format or location. The SDK does not migrate local data from the old VFS. After you connect the new database, the SDK downloads synced data again. Back up or migrate [local-only data](https://docs.powersync.com/client-sdks/advanced/local-only-usage.md#local-only-tables) separately; it is not downloaded again.

### Automatic VFS Selection

The `'auto'` option is available in `@powersync/web` version 2.7.0 and later. Use it for new apps that need broad browser support. It selects [`SimpleOpfsVFS`](#simpleopfsvfs) when OPFS is available and web workers are enabled. If your app primarily targets Safari, use [`OPFSCoopSyncVFS`](#opfscoopsyncvfs) for now.

Switching from `SimpleOpfsVFS` to `'auto'` reuses the database if `'auto'` selects that VFS and you keep the file name and storage location unchanged. The SDK does not migrate data from other VFS implementations.

Example configuration:

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

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

### SimpleOpfsVFS

`WASQLiteVFS.SimpleOpfsVFS` stores data in OPFS and supports multiple tabs across major browsers. The SDK selects a coordination strategy based on the browser's capabilities.

This VFS doesn't support `ATTACH` for additional databases, and SQLite temporary data, such as materialized CTEs, always stays in memory.

To select this VFS explicitly, import `WASQLiteVFS` and set `database.vfs`:

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

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

### IDBBatchAtomicVFS (Default)

`WASQLiteVFS.IDBBatchAtomicVFS` stores data in IndexedDB. It is the default when you omit the `vfs` option. The SDK keeps this default for backward compatibility, so existing apps continue to open their IndexedDB databases without changing their configuration.

Use this VFS if your app cannot use database workers and requires persistence. Unlike OPFS, IndexedDB is available without a database worker. See [PowerSyncDatabase Flags](https://docs.powersync.com/client-sdks/reference/javascript-web.md#available-flags) for the related configuration.

To select this VFS explicitly:

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

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

### OPFSCoopSyncVFS

`WASQLiteVFS.OPFSCoopSyncVFS` stores data in OPFS and supports multiple tabs across major browsers, including Safari on iOS.

Use this VFS if your app primarily targets Safari. On Safari, the database can fail to open after a page reloads or navigates away, because Safari doesn't always release locks held by the previous page's workers. Both this VFS and `SimpleOpfsVFS` can run into this issue, but `SimpleOpfsVFS` is more likely to. For broad browser support, use [`vfs: 'auto'`](#automatic-vfs-selection-recommended).

To select this VFS explicitly:

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

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

### OPFSWriteAheadVFS

`WASQLiteVFS.OPFSWriteAheadVFS` stores data in OPFS and uses its own write-ahead log so reads can run while a write is in progress. The SDK opens additional read-only connections in separate workers, which lets multiple queries run at the same time. Use it if you need concurrent reads and only target Chromium-based browsers, such as Chrome or Edge.

The `additionalReaders` option sets how many read-only connections the SDK opens in addition to the main connection. It defaults to `1`. Increase it if your app often runs many read queries at once. Each additional reader uses more memory.

This example opens two additional readers. When the main connection is idle, it can serve a third read query:

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

### AccessHandlePoolVFS

`WASQLiteVFS.AccessHandlePoolVFS` stores data in OPFS but does not support sharing the database across tabs. It is retained for backward compatibility and is not recommended for new apps.

### InMemoryVfs

`WASQLiteVFS.InMemoryVfs` stores database files in memory, avoiding the overhead of reading and writing them through OPFS or IndexedDB. Use it for development or online-only apps with small datasets when you don't need persistence. It is available in `@powersync/web` version 1.39.0 and later.

Starting with an empty database can help you reproduce issues from a clean state during development. If tabs share an in-memory database through a shared worker, closing the last tab using it clears the database. Otherwise, each tab has its own database, which is cleared when that tab closes or reloads.

Any mutations that haven't uploaded are lost when the database is cleared. A new in-memory database must download its synced data again after you connect, so consider how much data your app needs at startup.

> [!WARNING]
>
> In production, consider watching `SELECT * FROM ps_crud LIMIT 1` to detect pending mutations and show users when the upload queue is not empty. A [`beforeunload` listener](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event#usage_notes) can request confirmation before closing a tab with pending mutations, but it is not reliable on all platforms.

If you disable `enableMultiTabs`, only one database with a given name syncs at a time. Give each tab a unique database file name to let every tab sync independently:

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

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

## Multiple Tab Support

Think of PowerSync's multi-tab behavior as two separate layers:

1. **Local database access:** The VFS determines whether tabs can access the same local database and how they coordinate that access.
2. **Sync connection:** The `enableMultiTabs` option controls whether tabs share one sync connection through a shared worker. Without a shared worker, only one tab syncs at a time for a given database name.

PowerSync supports multi-tab database access on most desktop browsers. When shared-worker mode is enabled, the SDK creates a shared worker named `shared-powersync-[dbFileName]` to manage one sync connection for the tabs.

The shared sync worker connects to the PowerSync Service and applies downloaded changes on behalf of all tabs. It calls the `fetchCredentials` and `uploadData` methods from the connector provided by the most recently registered tab that is still open. When that tab closes, the worker uses the connector from another registered tab.

> [!WARNING]
>
> * The SDK disables shared-worker sync by default on Android, iOS, and Safari. See [Behavior Without Shared Workers](#behavior-without-shared-workers).
> * If your app primarily targets Safari, use [`OPFSCoopSyncVFS`](#opfscoopsyncvfs) for multi-tab database access.

### Behavior Without Shared Workers

PowerSync does not use shared-worker mode when the browser does not support it, the SDK disables it by default for the platform, or you set `enableMultiTabs: false`. To disable it explicitly:

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

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

Without shared-worker mode, each tab manages its own sync implementation, but only one tab syncs at a time for a given database name. Whether tabs access the same local data still depends on the VFS. With `InMemoryVfs`, each tab has its own database.

The SDK uses broadcast channels to share watched-query updates between tabs. Since version 2.1.0, it also shares sync status and Sync Stream subscriptions this way. This coordination is less reliable than using a shared worker, so updates may not reach every tab.

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

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 run in parallel, including while the writer is writing. This helps when queries overlap, but it does not make an individual query faster. Additional workers increase initialization time and memory use.

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`. The connection pool cannot be created without them.

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

1. You need to run read queries concurrently.
2. Your dataset is small enough to download again whenever a tab opens.
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.
5. You don't need multiple tabs to share offline state.

Import the pool from `@powersync/web/extra/shared-memory-pool` and pass it through the `opened` option:

```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

The following utility functions can help you manage OPFS storage programmatically:

```js theme={null}
// Clear all OPFS storage
async function purgeVFS() {
  await db.disconnect();
  await db.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}`);
  }
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.