DATE:
AUTHOR:
PowerSync Product Team
React Native SDK JavaScript/Web SDK Node.js SDK Capacitor SDK

PowerSync JavaScript SDKs: v2.0

DATE:
AUTHOR: PowerSync Product Team

We've released v2.0 of the PowerSync JavaScript SDKs, covering React Native, web, Node.js and Capacitor. This is a new major version, and the result of a substantial refactor focused on a more stable and intuitive public API, and simpler setup when getting started with the SDKs.

The biggest driver was the public API itself. @powersync/common used to expose the SDK's internals alongside public classes, so routine changes on our side could break your code or cause confusing errors. v2.0 keeps the public API and the implementation separate.

We also used v2.0 to fix several long-standing problems: we consolidated the option types that were spread across multiple nested interfaces so it's clearer where each option comes from, reduced the default bundle size, and made the API more consistent across our JavaScript packages and with our other SDKs.

This is a major release with breaking changes across the JavaScript packages. For most apps the upgrade is only a few small, mechanical changes — mainly to how you construct the database, call connect(), and set up logging. We cover all of them below.

Highlights of v2.0

  • A cleaner, more stable public API. @powersync/common used to leak the SDK's internals alongside its public API; now it exposes only public interfaces, with the implementation in a separate package. Future updates are much less likely to break your app, and you should see fewer errors. (PR #984, PR #993)

  • Simpler, more intuitive setup. Several options used to be scattered across many types; now they're consolidated. Logging is a smaller, clearer interface. (PR #987; Proposal #948, PR #966)

  • Smaller bundles. WebSockets are only used where necessary, and no longer loaded by default outside of React Native. This also reduces the default bundle size. (Proposal #950, PR #1002)

  • React Native: OP-SQLite is built in. OP-SQLite used to be a separate package wired up through an open factory. In v2.0 it's the default driver, bundled into @powersync/react-native, so you set its options directly on the constructor. (PR #1010)

  • A consistent API across platforms and SDKs. Shared types now work across all the JavaScript packages, and sync status and options match our other SDKs. (Request #572, PR #999)

  • Easier custom database adapters. Implement one method instead of many. (PR #995)

  • Deprecated APIs cleaned up. We used this opportunity to remove long-deprecated syntax, like the old Schema v1 table constructors. (PR #1004, PR #1013)

In detail

A cleaner, more stable public API

@powersync/common was treated as a stable public package, but in practice it exported the SDK's internals alongside its public API: @internal members, abstract classes, and other implementation details. Under TypeScript's structural rules, that made even subtle changes breaking. Adding a method to AbstractPowerSyncDatabase, for example, counted as a breaking change.

We cleaned this up in two steps. First, we adopted @microsoft/api-extractor and tagged every export as @public or @internal, making the public API explicit (PR #984). Then we split the package: @powersync/common now holds only the stable public interfaces, and the implementation moves to a private @powersync/shared-internals package we're free to change (PR #993).

It also clears up a confusing set of type errors. For example, Type 'AbstractPowerSyncDatabase' is not assignable to type 'AbstractPowerSyncDatabase' could show up when you had more than one copy of @powersync/common in your dependency tree. Because the public types are interfaces now, TypeScript treats those copies as the same type, so the errors no longer appear.

Simpler, more intuitive setup

The options passed to PowerSyncDatabase needed the most work: they were confusing, didn't autocomplete well, and differed between SDKs. They were spread across too many interfaces — PowerSyncDatabaseOptions extends BasePowerSyncDatabaseOptions extends AdditionalConnectionOptions — so there was no single place to find everything available. The database field accepted a DBAdapter, an SQLOpenFactory, or SQLOpenOptions all at once, so autocomplete on the options object showed every DBAdapter method instead of the options you wanted. And because each platform-specific factory supported a different set of options, advanced ones like journalMode and cacheSize were hard to find.

v2.0 splits opening the database into three mutually exclusive keys: database for common options like the filename, factory for a custom SQLOpenFactory, and opened for an already-open DBAdapter. This also rules out invalid combinations, like passing an encryptionKey alongside a pre-opened adapter. Sync options — previously spread across BaseConnectionOptions, AdditionalConnectionOptions, and several internal types — collapse into a single public SyncOptions interface that mirrors our Dart SDK.

Logging is more intuitive too. The old js-logger dependency wasn't widely used, and its unusual TypeScript definitions caused problems when bundling. In its place is a small PowerSyncLogger interface that takes the log level as a named parameter, rather than one method per level. Instead of configuring a global logger, you create one and pass it to the database; the default forwards to console.log with a [PowerSync] prefix at info level and above.

Smaller bundles, and no required bundler step

WebSocket support relies on RSocket, a complex protocol whose packages are around 500 kB and which needs custom build steps to work outside Node.js. It was the main reason the SDK shipped with a bundler at all. We added it for React Native, where streaming HTTP responses aren't always reliable, but most platforms don't need it. In v2.0 it's no longer bundled into the main SDK and loads only when you connect over WebSockets. Along with removing the legacy JavaScript sync client, dropping the CommonJS builds (PR #1011), and merging the two web workers into one (Proposal #949, PR #1008), these changes shrink the default bundle and remove the bundler step we had to maintain.

A consistent API across platforms, and across SDKs

Sharing code used to mean duplicating type annotations: a backend connector typed against @powersync/web and one typed against @powersync/react-native read as different types. v2.0 adds CommonPowerSyncDatabase in @powersync/common as a single interface to type cross-platform code against, so one connector works across all the JavaScript packages. SyncStatus and the connection options now also match our Dart, Swift, and Kotlin SDKs.

Clearing deprecated APIs

A major version is a good time to remove APIs that have been deprecated for a while. v2.0 drops the old Schema v1 constructor syntax (new Column(...), new Index(...)) in favor of the object syntax, and TableV2Options is renamed to TableOptions. It also removes React Native Quick SQLite support, and the old open-factory classes and catch-all flags option (replaced by the clearer options above). The breaking changes below cover what to update.

Breaking changes and how to upgrade

1. Sync and open options have been refactored

Sync options are now set only on connect(), through a single SyncOptions interface. They are no longer set when constructing the database.

// Before: sync options on the constructor
new PowerSyncDatabase({ database: { dbFilename: 'test.db' }, retryDelayMs: 1000 });

// After: sync options on connect()
const db = new PowerSyncDatabase({ database: { dbFilename: 'test.db' } });
await db.connect(connector, { retryDelayMs: 1000 });

Passing a custom open factory now uses a factory key instead of database. Passing an already-opened DBAdapter uses an opened key. This is the main change for Capacitor apps, which pass a platform-specific factory:

// Before (Capacitor): factory via `database`
import { WASQLiteOpenFactory, CapacitorSQLiteOpenFactory } from '@powersync/capacitor';

const db = new PowerSyncDatabase({
  schema: AppSchema,
  database: isWeb
    ? new WASQLiteOpenFactory({ dbFilename: 'mydb.sqlite' })
    : new CapacitorSQLiteOpenFactory({ dbFilename: 'mydb.sqlite' })
});

// After (Capacitor): via `factory`
const db = new PowerSyncDatabase({
  schema: AppSchema,
  factory: isWeb
    ? new WASQLiteOpenFactory({ dbFilename: 'mydb.sqlite' })
    : new CapacitorSQLiteOpenFactory({ dbFilename: 'mydb.sqlite' })
});

On the web, options that previously required a WASQLiteOpenFactory are now available directly on database, and the flags field (and the WebSQLFlags type) has been removed. Options that lived under flags have moved to more specific keys.

// Before: VFS options needed a factory
new PowerSyncDatabase({
  schema: AppSchema,
  database: new WASQLiteOpenFactory({
    dbFilename: 'exampleVFS.db',
    vfs: WASQLiteVFS.OPFSWriteAheadVFS,
    additionalReaders: 2
  })
});

// After: set them directly on `database`
new PowerSyncDatabase({
  schema: AppSchema,
  database: {
    dbFilename: 'exampleVFS.db',
    vfs: WASQLiteVFS.OPFSWriteAheadVFS,
    additionalReaders: 2
  }
});
// Before: misc options under `flags`
new PowerSyncDatabase({
  schema: AppSchema,
  database: { dbFilename: 'test.db' },
  flags: { broadcastLogs: true, disableSSRWarning: true }
});

// After: moved to more specific keys
new PowerSyncDatabase({
  schema: AppSchema,
  database: {
    dbFilename: 'test.db',
    disableSSRWarning: true
  },
  broadcastLogs: true
});

2. HTTP is now the default connection method

The default sync connection method changes from WebSockets to HTTP. On the web, Node.js, and Expo React Native apps, you don't need to change anything.

React Native without Expo is the exception: HTTP streaming needs a streaming fetch, which plain React Native doesn't provide. Expo apps get one from expo/fetch, which the SDK now uses by default, and non-Expo apps fall back to WebSockets automatically. You can also set the connection method explicitly when connecting:

await db.connect(connector, { connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET });

3. Logging API replaced

We removed the js-logger dependency, so createBaseLogger(), useDefaults(), and setLevel() no longer exist. Create a logger and pass it to the database instead:

// Before
import { createBaseLogger, LogLevel } from '@powersync/web';
const logger = createBaseLogger();
logger.useDefaults();
logger.setLevel(LogLevel.DEBUG);

// After
import { createConsoleLogger, LogLevels } from '@powersync/web';
const logger = createConsoleLogger({ minLevel: LogLevels.debug });
const db = new PowerSyncDatabase({ database: { dbFilename: 'test.db' }, logger });

For a custom logger, implement the PowerSyncLogger interface and pass that instead. On the web, the database and sync workers take their log levels separately: databaseWorkerLogLevel on the database options, and logLevel on the sync options.

4. React Native: OP-SQLite is built in, RNQS is removed

@powersync/op-sqlite is no longer a separate package. The OP-SQLite adapter is part of @powersync/react-native and is the default driver, so its options live directly on the constructor.

// Before
const db = new PowerSyncDatabase({
  database: new OPSqliteOpenFactory({ dbFilename: 'test.db' })
});

// After
const db = new PowerSyncDatabase({
  database: { dbFilename: 'test.db' }
});

To upgrade, drop your dependency on @journeyapps/react-native-quick-sqlite and @powersync/op-sqlite. If you don't depend on it already, add @op-engineering/op-sqlite as a new dependency.

We also removed polyfills the SDK used to bundle for older React Native versions (TextEncoder/TextDecoder, react-native-fetch-api, ReadableStream).

5. Web: a single worker file, and the UMD build is removed

The database and sync workers are now served from one file. Apps built with webpack or vite using the default worker need no change.

If you copy the workers into your app, run powersync-web copy-assets again and update your paths from /@powersync/worker/WASQLiteDB.umd.js and /@powersync/worker/SharedSyncImplementation.umd.js to /@powersync/worker.js.

The UMD build of @powersync/web has been removed. Import @powersync/web directly and remove any import mappings that point at @powersync/web/umd. React Native Web on Expo also needs config.resolver.unstable_conditionsByPlatform.web.push('react-native-web'); in metro.config.js.

6. SyncStatus fields aligned with other SDKs

uploading, downloading, uploadError, and downloadError have moved from the dataFlowStatus sub-object onto SyncStatus directly.

// Before
status.dataFlowStatus.downloading;
status.dataFlowStatus.downloadError;

// After
status.downloading;
status.downloadError;

The dataFlowStatus getters still work but are deprecated.

7. Schema v1 syntax removed

The deprecated v1 Table syntax, where columns and indexes were passed as arrays of Column, Index, and IndexedColumn instances, has been removed. Use the object syntax instead, and rename TableV2Options to TableOptions.

// Before: v1 array syntax
new Table({
  name: 'todos',
  columns: [new Column({ name: 'list_id', type: ColumnType.TEXT })],
  indexes: [new Index({ name: 'list', columns: [new IndexedColumn({ name: 'list_id' })] })]
});

// After: object syntax
new Table(
  { list_id: column.text },
  { indexes: { list: ['list_id'] } }
);

To build a table from column arrays at runtime, use the new ResolvedTable class.

8. Other breaking changes

These affect a smaller set of apps:

  • Custom database adapters need less code. DBAdapter and LockContext are now abstract classes instead of interfaces. A custom adapter only implements executeRaw; the base class builds the other query methods on top of it. QueryResult is now generic (QueryResult<T>) and exposes its rows as an array, and table-change events are reported as a single changedTables: string[].

  • Some public types are now interfaces. AbstractPowerSyncDatabase is renamed to CommonPowerSyncDatabase and is now an interface; the old name stays as a type alias, but use CommonPowerSyncDatabase for cross-platform code. CrudEntry and SyncStatus are interfaces now too, so you can't construct them directly (CrudEntry.fromRow is removed).

  • The deprecated React hooks are removed. usePowerSyncQuery, usePowerSyncWatchedQuery, and usePowerSyncStatus are removed from @powersync/react. Use useQuery and useStatus instead.

  • Packages ship ESM only. The CommonJS builds are removed, including from @powersync/kysely-driver and @powersync/attachments. Most setups need no change — modern bundlers and current Node.js both load ESM.

Getting started

The v2.0 changes were released in the following versions of the SDKs:

  • @powersync/react-native@2.0.0

  • @powersync/web@2.0.0

  • @powersync/node@0.20.0

  • @powersync/capacitor@0.8.0

New to PowerSync? Start with the Setup Guide.

Upgrading from v1.x? Most apps need only small changes to how they construct the database, connect, and set up logging. Review the breaking changes above to see which apply to you.

Feedback and help

A lot of what went into v2.0 came directly from your feedback so please keep it coming. If something doesn't work the way you expect, or you run into issues after upgrading, let us know.

Come chat with us on Discord, or open an issue on GitHub.

Powered by LaunchNotes