PowerSync JS Demos
repository·main·Indexed 20 days ago
https://github.com/powersync-ja/powersync-jsA collection of implementation examples for the PowerSync sync engine, featuring integrations with Angular, Supabase, Capacitor (iOS/Android), Electron (Main and Renderer processes), and Next.js. Includes guides on configuring workers, JWT authentication, and local-first development using SQLite and server-side databases like Postgres or MySQL.
What's inside powersync-js
- PowerSync is a service and a set of SDKs designed to help developers build offline-first, real-time, reactive applications. It works by keeping a Postgres database in sync with an on-device SQLite database, ensuring that the application can function seamlessly even with poor or no connectivity.
Overview of PowerSync SDK common JS
mainThe
@powersync/commonpackage provides core TypeScript implementations used across PowerSync's JavaScript-based SDKs. It includes:- A TypeScript implementation of a PowerSync database connector.
- A streaming sync bucket implementation.
- Attachment utilities.
Overview of PowerSync JavaScript SDKs
mainPowerSync is a sync engine designed for building local-first applications. It enables an instantly-responsive UI/UX by syncing data between a client-side SQLite database and a server-side database (such as Postgres, MongoDB, MySQL, or SQL Server).
The
powersync-jsmonorepo provides SDKs for various environments including:- React Native: Mobile implementation.
- Web: JavaScript Web SDK.
- Node.js: Server-side/CLI implementation.
- Framework Integrations: React, Vue, and TanStack Query (React Query).
- ORM Drivers: Kysely and Drizzle integrations.
- Attachments: Utilities for handling file attachments.
Project Structure of the Nuxt Supabase Demo
mainThe following directory structure outlines the key files in this demo:
powersync/: ContainsAppSchema.ts(schema definition) andSuperbaseConnector.ts(Supabase connector implementation).plugins/powersync.client.ts: PowerSync client-side plugin setup.pages/: Application routes (index.vue,login.vue,confirm.vue).components/AppHeader.vue: UI components.db/seed.sql: Database initialization scripts.powersync.yaml: PowerSync server configuration.sync-config.yaml: PowerSync sync stream definitions.nuxt.config.ts: Nuxt framework configuration.
Use the PowerSync SQL-JS Adapter for development
mainThe
@powersync/adapter-sql-jspackage provides a pure JavaScript SQLite implementation using SQL.js. It is designed to streamline the development workflow by eliminating the need for native dependencies, making it ideal for environments like Expo Go or other JavaScript-only runtimes.⚠️ Important Limitations
- Performance: This adapter is significantly slower than native adapters. Every write operation triggers a complete rewrite of the entire database file to persistent storage.
- Reliability: It does not provide SQLite consistency guarantees. If the application is killed during a write operation, you may experience data loss or database corruption.
- Production Recommendation: For production React Native apps, use the default adapter in
@powersync/react-native(based on OP-SQLite) for substantially better performance and reliability.
Configure a DatabaseSource
mainThe
DatabaseSourcetype defines how a PowerSync database can be initialized. It supports three modes:- Opened: Providing an existing
DBAdapterinstance. - Factory: Providing a
SQLOpenFactoryto create the adapter. - Database: Providing
OpenOptions(configuration) to initialize the database.
// Example of the three shapes of DatabaseSource: // 1. Using an existing adapter const source1: DatabaseSource = { opened: myDbAdapter }; // 2. Using a factory const source2: DatabaseSource = { factory: mySqlOpenFactory }; // 3. Using configuration options const source3: DatabaseSource = { database: { path: 'my-db.sqlite' } };- Opened: Providing an existing
Prevent Suspense fallback when updating query parameters
mainWhen you change the parameters of a
useSuspenseQuery(e.g., changing a filter or a limit), the hook restarts and enters a suspending state, which triggers the<Suspense>fallback.To keep displaying the current (stale) data while the new query is fetching, you should wrap the parameter update in React's
startTransitionor use theuseDeferredValuehook. This prevents the UI from jumping back to the loading state.// Example using startTransition to prevent fallback import { ErrorBoundary } from 'react-error-boundary'; import React, { Suspense } from 'react'; import { useSuspenseQuery } from '@powersync/react'; const TodoListContent = () => { const [query, setQuery] = React.useState('SELECT * FROM lists'); const { data: todoLists } = useSuspenseQuery(query); return ( <div > <button onClick={() => { React.startTransition(() => setQuery('SELECT * from lists limit 1')); }}> Update </button> <ul >{todoLists.map((list) => ( <li key={list.id}>{list.name}</li ))} </ul </div ); }; export const TodoListDisplaySuspense = () => { return ( <ErrorBoundary fallback={<div>Something went wrong</div>}> <Suspense fallback={<div>Loading todo lists...</div>}> <TodoListContent /> </Suspense> </ErrorBoundary> ); };Use PowerSync Attachments
mainThe@powersync/commonpackage includes utilities for handling attachments. For detailed information on how to use the attachments API, refer to the dedicated Attachments README.How Optional Sync works using local-only tables and viewName
mainThe demo implements a pattern to allow users to use the app without an account, then seamlessly sync their data once they register.
Core Concept: Local-only Tables
The app uses local-only tables to persist data before registration. These tables do not log updates in the upload queue, preventing unnecessary database growth.
The Recommended Implementation Pattern
To avoid changing application queries when a user signs in, the app uses the
viewNameproperty to override table names.- Local-only State: Local tables (e.g.,
local_lists) have theirviewNameset to the standard table name (e.g.,lists). The app queriesSELECT * FROM lists, which points to the local table. The actual synced tables are assigned aninactive_synced_view name so they are ignored. - Transition: When the user signs in,
updateSchemais called to change theviewNameof the synced tables back to the standard names (e.g.,lists). - Data Migration: Data is copied from the local-only tables to the synced tables, and the local-only data is deleted to save space. The synced tables then automatically begin uploading data to the backend.
Limitations
updateSchemacannot be called inside a transaction. It is recommended to perform schema updates when the database is not connected.
- Local-only State: Local tables (e.g.,
Platform support for PowerSync Capacitor SDK
mainThe
@powersync/capacitorSDK supports the following platforms with different underlying drivers:Platform Driver Implementation Android Native SQLite via Capacitor Community SQLiteiOS Native SQLite via Capacitor Community SQLiteWeb WASQLite via PowerSync Web SDKElectron WASQLite via PowerSync Web SDKUse Incremental Queries to optimize data references
mainBy default,
useQuerytriggers a newdataarray reference whenever any change is detected in the underlying tables, even if the specific query result hasn't changed. This can cause frequent re-renders.Incremental Queries solve this by performing in-memory comparisons. When configured with a
rowComparator, thedataarray reference remains the same if the result set is unchanged. Additionally, individual row object references are preserved for unchanged rows.To enable this, provide a
rowComparatorobject in the options:keyBy: A function to extract a unique key from an item.compareBy: A function to determine if an item has changed (e.g., usingJSON.stringify).
const { data, isLoading, isFetching } = useQuery( `SELECT * FROM cats WHERE breed = 'tabby'`, [], { rowComparator: { keyBy: (item) => item.id, compareBy: (item) => JSON.stringify(item) } } )Implement the watchAttachments callback
mainThe
watchAttachmentscallback is a required configuration forAttachmentQueue. It tells the queue which attachments it is responsible for (downloading, uploading, or archiving) by providing a list ofWatchedAttachmentItemobjects.Important: Each item in the array must provide either
fileExtensionORfilename, but not both.WatchedAttachmentItem Types:
{ id: string; fileExtension: string; metaData?: string; mediaType?: string; }{ id: string; filename: string; metaData?: string; mediaType?: string; }
Example Implementation: Use a PowerSync
db.watch()query to detect changes in your application tables (e.g., auserstable containing aphoto_id) and map those rows toWatchedAttachmentItemobjects.watchAttachments: (onUpdate) => { // Watch photo references in users table db.watch( 'SELECT photo_id, metadata FROM users WHERE photo_id IS NOT NULL', [], { onResult: async (result) => { const attachments = result.rows?._array.map(row => ({ id: row.photo_id, fileExtension: 'jpg', metaData: row.metadata })) ?? []; await onUpdate(attachments); } } ); }