diff --git a/AGENTS.md b/AGENTS.md index 5ccbd4e..8b7beab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,6 @@ That file includes: **Do not** assume Supabase, assume self-hosted Docker, or skip CLI steps unless the user **explicitly** opts out. -**`powersync login`** authenticates the CLI to **PowerSync Cloud** only (personal access token). It is **not** how you “log in” to a **self-hosted** PowerSync service. Self-hosted doesnt not require login +**`powersync login`** is **PowerSync Cloud only**. Self-hosted does not use it — see `skills/powersync/references/powersync-cli.md` § “Authentication”. When editing files under `skills/powersync/`, preserve and strengthen playbook language so agents cannot reasonably treat references as optional recipes. diff --git a/skills/powersync/AGENTS.md b/skills/powersync/AGENTS.md index 0150cd0..98d3333 100644 --- a/skills/powersync/AGENTS.md +++ b/skills/powersync/AGENTS.md @@ -12,26 +12,16 @@ Use this skill to onboard a project onto PowerSync without trial-and-error. Trea | Use the **PowerSync CLI** to scaffold, link (if cloud hosted), and deploy (`references/powersync-cli.md`) | Hand-write `service.yaml` / `sync-config.yaml` from scratch or invent compose files **unless** the user explicitly says they cannot use the CLI | | **Stop and ask** when a step needs credentials or interactive Cloud login you cannot perform | Silently build an alternate stack (e.g. manual Docker) without user confirmation | | Complete **backend readiness** (deployed sync config, auth, publication) **before** app code | Start React/client integration while sync is still unconfigured | +| Use **Sync Streams** (`config: edition: 3`) for new projects | Generate legacy Sync Rules YAML for new projects | If the user wants a shortcut, they must **say so explicitly** (e.g. “I can’t use the CLI, give dashboard steps only”). ## Always Use the PowerSync CLI -**The [PowerSync CLI](https://docs.powersync.com/tools/cli.md) is the default tool for all PowerSync operations.** Do not manually create config files, do not direct users to the dashboard, and do not write service.yaml or sync-config.yaml from scratch. The CLI handles all of this. +**The [PowerSync CLI](https://docs.powersync.com/tools/cli.md) is the default tool for all PowerSync operations.** Do not manually create config files, do not direct users to the dashboard, and do not write `service.yaml` or `sync-config.yaml` from scratch. Only fall back to manual config or dashboard instructions when the user explicitly says they can't use the CLI. -What the CLI does — use it instead of doing these things manually: - -- **Create a Cloud instance:** `powersync init cloud` → `powersync link cloud --create --project-id=` (scaffolds config, creates the instance, and links it in one flow) -- **Link an existing Cloud instance:** `powersync pull instance --project-id= --instance-id=` (downloads config to local files) -- **Create a local self-hosted instance:** `powersync init self-hosted` → `powersync docker configure` → `powersync docker start` (spins up a full local PowerSync stack via Docker — no manual Docker setup needed) -- **Deploy config:** `powersync deploy service-config`, `powersync deploy sync-config` (validates and deploys — don't copy-paste YAML to a dashboard) -- **Generate client schema:** `powersync generate schema --output=ts` (generates TypeScript schema from deployed sync config — don't write it by hand) -- **Generate dev tokens:** `powersync generate token --subject=user-1` (for local testing — don't hardcode JWTs) - -Only fall back to manual config or dashboard instructions when the user explicitly says they can't use the CLI. - -Full CLI reference: `references/powersync-cli.md` — **always load this file** when setting up or modifying a PowerSync instance. +**Always load `references/powersync-cli.md`** when setting up or modifying a PowerSync instance — it contains the full command reference for Cloud, self-hosted, and Docker workflows. ## Onboarding Playbook @@ -50,6 +40,17 @@ When the task is to add PowerSync to an app, follow this sequence in order: Do not start client-side debugging while the PowerSync service is still unconfigured. If the UI is stuck on `Syncing...`, the default diagnosis is incomplete backend setup, not a frontend bug. +## Install Latest Dependencies + +Always install PowerSync packages with `@latest` to get critical fixes and the most current API: + +```bash +npm install @powersync/web@latest # or react-native, node, etc. +npm install @journeyapps/wa-sqlite@latest +``` + +Never omit `@latest` for `@powersync/*` and `@journeyapps/*` packages. These packages release frequently and older cached versions can be missing critical fixes or new APIs the sync config depends on. + ## Critical Footguns These apply to all paths. Domain-specific pitfalls are documented in the relevant reference files — only load those when working on that domain. @@ -138,92 +139,17 @@ If the backend is Supabase, also load `references/supabase-auth.md`. ### Path 3: Self-Hosted + CLI (Recommended) -**Not Cloud:** do not use **`powersync login`** as the way to “log in” to self-hosted — that command stores a **PowerSync Cloud** PAT. Self-hosted uses **`powersync init self-hosted`**, **`powersync docker configure`**, **`powersync docker start`**, and the service’s **`PS_ADMIN_TOKEN`** for admin API access. - -Load `references/powersync-cli.md`, `references/powersync-service.md`, and `references/sync-config.md`. Prefer the CLI for Docker runs (`powersync docker run`, `powersync docker reset`), schema generation, and any supported self-hosted operations. See [PowerSync CLI](https://docs.powersync.com/tools/cli.md). +Load `references/powersync-cli.md`, `references/powersync-service.md`, and `references/sync-config.md`. Prefer the CLI for Docker runs (`powersync docker run`, `powersync docker reset`), schema generation, and any supported self-hosted operations. Remember: **`powersync login` is Cloud-only** — see `references/powersync-cli.md` § “Authentication” for self-hosted auth. ### Path 4: Self-Hosted + Manual Docker Only when the CLI cannot be used. Load `references/powersync-service.md` and `references/sync-config.md`. If the backend is **not** Supabase, also load `references/custom-backend.md`. -## Architecture - -```mermaid -flowchart LR - - subgraph BACKEND["Your Backend"] - direction TB - DB["Backend Database (Postgres | MongoDB | MySQL | Supabase | …)"] - API["Backend API (Your server / cloud functions)"] - API -- "Applies writes" --> DB - end - - subgraph PS_SERVICE["PowerSync Service"] - direction TB - SYNC["Partial Sync (sync rules filter data per user)"] - end - - subgraph APP["Your App"] - direction TB - SDK["PowerSync SDK"] - SQLITE["In-app SQLite (local replica — reads are instant)"] - QUEUE["Upload Queue (offline write buffer)"] - UI["UI"] - SDK --- SQLITE - SDK --- QUEUE - SQLITE <--> UI - QUEUE <--> UI - end - - DB -- "Replicates changes (CDC / logical replication)" --> PS_SERVICE - PS_SERVICE -- "Streams changes (real-time sync)" --> SDK - QUEUE -- "Uploads writes (when connectivity resumes)" --> API -``` +## Architecture, Routing, SDK Tables & Key Rules + +These are defined once in **SKILL.md** — refer there for: -Key rule: **client writes never go through PowerSync**. They go from the app's upload queue to your backend API. PowerSync handles the read and sync path only. - -## What to Load for Your Task - -| Task | Start with | Load on demand | -|------|-----------|----------------| -| Supabase + PowerSync | `references/onboarding-supabase.md` | `references/supabase-auth.md`, `references/sync-config.md`, SDK files (when writing app code) | -| Custom backend (non-Supabase) | `references/onboarding-custom.md` | `references/custom-backend.md`, `references/sync-config.md`, SDK files (when writing app code) | -| New project setup | `references/powersync-cli.md` + `references/powersync-service.md` | `references/sync-config.md`, SDK files (when writing app code) | -| Self-hosting / service config | `references/powersync-service.md` + `references/powersync-cli.md` | `references/sync-config.md` | -| Writing sync config | `references/sync-config.md` | — | -| Debugging sync issues | `references/powersync-debug.md` | — | -| Attachments | `references/attachments.md` | — | -| Architecture overview | This file is sufficient | `references/powersync-overview.md` for deep links | - -## SDK Reference Files - -### JavaScript / TypeScript - -Always load `references/sdks/powersync-js.md` for any JS/TS project, then load the applicable framework file. - -| Framework | Load when… | File | -|-----------|-----------|------| -| React / Next.js | React web app, Next.js, or **any Vite + React project** (load before package install — contains required `vite.config.ts` with `optimizeDeps.exclude` and `worker.format: 'es'`) | `references/sdks/powersync-js-react.md` | -| React Native / Expo | React Native, Expo, or Expo Go | `references/sdks/powersync-js-react-native.md` | -| Vue / Nuxt | Vue or Nuxt | `references/sdks/powersync-js-vue.md` | -| Node.js / Electron | Node.js CLI/server or Electron | `references/sdks/powersync-js-node.md` | -| TanStack | TanStack Query or TanStack DB | `references/sdks/powersync-js-tanstack.md` | - -### Other SDKs - -| Platform | Load when… | File | -|----------|-----------|------| -| Dart / Flutter | Dart / Flutter | `references/sdks/powersync-dart.md` | -| .NET | .NET | `references/sdks/powersync-dotnet.md` | -| Kotlin | Kotlin | `references/sdks/powersync-kotlin.md` | -| Swift | Swift / iOS / macOS | `references/sdks/powersync-swift.md` | - -## Key Rules to Apply Without Being Asked - -- Use Sync Streams for new projects. Sync Rules are legacy. -- Never define the `id` column in a PowerSync table schema; it is created automatically. -- Use `column.integer` for booleans and `column.text` for ISO date strings. -- `connect()` is fire-and-forget. Use `waitForFirstSync()` if you need readiness. -- `transaction.complete()` is mandatory or the upload queue stalls permanently. -- `disconnectAndClear()` is required on logout or user switch when local data must be wiped. -- A 4xx response from `uploadData` blocks the upload queue permanently; return 2xx for validation errors. +- **Architecture diagram** — shows the read/sync path (PowerSync Service → SDK) and write path (upload queue → your backend API) +- **"What to Load for Your Task"** table — maps tasks to starter files and on-demand files +- **SDK Reference Files** tables — maps frameworks/platforms to reference files +- **"Key Rules to Apply Without Being Asked"** — `id` column, `connect()`, `transaction.complete()`, `disconnectAndClear()`, 4xx upload handling diff --git a/skills/powersync/SKILL.md b/skills/powersync/SKILL.md index 4ecef04..134aed8 100644 --- a/skills/powersync/SKILL.md +++ b/skills/powersync/SKILL.md @@ -1,6 +1,6 @@ --- name: powersync -description: Guided onboarding and best practices for building applications with PowerSync — Cloud and self-hosted setup, sync configuration, client SDK usage, backend integration (Supabase, custom Postgres, MongoDB, MySQL, MSSQL), and debugging. +description: Guided onboarding and best practices for building applications with PowerSync — Cloud and self-hosted setup, sync configuration, client SDK usage, backend integration (Supabase, custom Postgres, MongoDB, MySQL, MSSQL), and debugging. Use this skill whenever the user mentions PowerSync, offline-first sync, local-first architecture, sync rules, sync streams, uploadData, fetchCredentials, real-time data replication, or wants to add offline-capable sync to a mobile or web app — even if they don't explicitly name PowerSync. license: MIT compatibility: Works with any skills-compatible agent. Some references include CLI commands requiring the @powersync/cli package. metadata: @@ -14,7 +14,7 @@ metadata: Use this skill to onboard a project onto PowerSync without trial-and-error. Treat this as a guided workflow first and a reference library second. -**Agents:** Follow **[AGENTS.md](AGENTS.md)** in full — including **Agent compliance** (ask Cloud vs self-hosted, ask backend if unspecified, CLI-first, no silent shortcuts). **`powersync login`** is **PowerSync Cloud only** (PAT); self-hosted does not use it. +**Agents: Read [AGENTS.md](AGENTS.md) before proceeding.** It contains the mandatory compliance rules and onboarding playbook. The Quick Rules below are a reminder, not a substitute. **`powersync login`** is **PowerSync Cloud only** (PAT); self-hosted does not use it. ## Quick Rules @@ -34,6 +34,7 @@ Use this skill to onboard a project onto PowerSync without trial-and-error. Trea | Self-hosting / service config | `references/powersync-service.md` + `references/powersync-cli.md` | `references/sync-config.md` | | Writing sync config | `references/sync-config.md` | — | | Debugging sync issues | `references/powersync-debug.md` | — | +| Raw Tables (advanced) | `references/raw-tables.md` | — | | Attachments | `references/attachments.md` | — | | Architecture overview | `references/powersync-overview.md` | — | @@ -50,6 +51,7 @@ Always load `references/sdks/powersync-js.md` for any JS/TS project, then load t | Vue / Nuxt | `references/sdks/powersync-js-vue.md` | | | Node.js / Electron | `references/sdks/powersync-js-node.md` | | | TanStack | `references/sdks/powersync-js-tanstack.md` | | +| Drizzle / Kysely ORM | `references/sdks/powersync-js-orm.md` | Project uses Drizzle or Kysely | ### Other SDKs diff --git a/skills/powersync/references/attachments.md b/skills/powersync/references/attachments.md index 58dd173..0cf0842 100644 --- a/skills/powersync/references/attachments.md +++ b/skills/powersync/references/attachments.md @@ -7,6 +7,17 @@ metadata: # PowerSync Attachments +> **Load this when** the app needs file uploads/downloads (images, documents, media) synced alongside PowerSync data. + +## Table of Contents +- [How It Works](#how-it-works) +- [Package Setup](#package-setup) +- [Schema Setup](#schema-setup) +- [Storage Adapters](#storage-adapters) +- [Initialize the Attachment Queue](#initialize-the-attachment-queue) +- [Upload / Delete / Access Files](#upload-a-file) +- [Error Handling](#error-handling) + PowerSync handles file attachments using a **metadata + storage provider** pattern: structured metadata syncs through PowerSync while actual files live in a purpose-built storage system (S3, Supabase Storage, Cloudflare R2, etc.). An offline-first queue manages uploads, downloads, and retries automatically in the background. | Resource | Description | diff --git a/skills/powersync/references/custom-backend.md b/skills/powersync/references/custom-backend.md index 7a4c9f3..41ddd04 100644 --- a/skills/powersync/references/custom-backend.md +++ b/skills/powersync/references/custom-backend.md @@ -7,6 +7,15 @@ metadata: # Custom Backend for PowerSync +> **Load this when** building a PowerSync integration without Supabase — custom auth, custom backend API, or any non-Supabase database. + +## Table of Contents +- [Architecture Recap](#architecture-recap) +- [1. Custom JWT Auth](#1-custom-jwt-auth) +- [2. Backend API for uploadData](#2-backend-api-for-uploaddata) +- [3. Client-Side Connector](#3-client-side-connector-custom-backend) +- [Common Pitfalls](#common-pitfalls) + Use this file when building a PowerSync integration **without Supabase** — your own auth and a backend API that receives writes from the client's upload queue. For **source database setup** (Postgres replication, MongoDB replica set, MySQL binlog, MSSQL CDC), see `references/powersync-service.md` § "Source Database Setup". diff --git a/skills/powersync/references/onboarding-custom.md b/skills/powersync/references/onboarding-custom.md index f9ad9e9..623ff19 100644 --- a/skills/powersync/references/onboarding-custom.md +++ b/skills/powersync/references/onboarding-custom.md @@ -7,6 +7,8 @@ metadata: # Custom Backend + PowerSync Onboarding +> **Load this when** onboarding an app onto PowerSync with a non-Supabase backend (custom Postgres, MongoDB, MySQL, MSSQL). + Use this recipe when onboarding any app onto PowerSync with a **non-Supabase backend** — your own database, your own auth, and your own backend API. Works for all platforms (web, React Native, Flutter, Kotlin, Swift, .NET, etc.) and both Cloud and self-hosted. **CLI-first.** See `references/powersync-cli.md`. Fall back to the dashboard (Cloud) or manual Docker config (self-hosted) only if the CLI is unavailable or the user explicitly prefers it. diff --git a/skills/powersync/references/onboarding-supabase.md b/skills/powersync/references/onboarding-supabase.md index dbfdb11..7672604 100644 --- a/skills/powersync/references/onboarding-supabase.md +++ b/skills/powersync/references/onboarding-supabase.md @@ -7,6 +7,8 @@ metadata: # Supabase + PowerSync Cloud Onboarding +> **Load this when** onboarding an app onto PowerSync Cloud with a Supabase backend. + Use this recipe when onboarding any app onto PowerSync Cloud with a Supabase backend. This works for all platforms (web, React Native, Flutter, Kotlin, Swift, .NET, etc.). **CLI-first.** See `references/powersync-cli.md`. Fall back to the dashboard only if the CLI is unavailable or the user explicitly prefers it. @@ -81,13 +83,17 @@ Only after Phase 2 is complete. ``` Or write manually — but never define the `id` column (it is automatic). -11. **Implement the backend connector.** See `references/supabase-auth.md` § "fetchCredentials()" for the Supabase-specific implementation. For `uploadData`, Supabase users can write directly to Supabase via the client library or use Edge Functions. +11. **Implement the backend connector.** See `references/supabase-auth.md` § "fetchCredentials()" and § "uploadData()" for complete implementations including error handling strategy. + + **Auth prerequisite:** `fetchCredentials()` requires an active Supabase session. Call `db.connect(connector)` only after the user has signed in. For apps without a sign-in screen, enable anonymous auth in Supabase Dashboard → Authentication → Providers → Anonymous. 12. **Initialize PowerSync and connect.** - `connect()` is fire-and-forget — use `waitForFirstSync()` if you need readiness. - Use `disconnectAndClear()` on logout or user switch. -13. **Switch reads to local SQLite** and test offline behavior. +13. **Development tokens** (optional, for testing without user auth): After deploying, run `powersync generate token --subject=` to get a short-lived JWT. On PowerSync Cloud, this works immediately after deploy — no extra `client_auth` config needed. Do not use dev tokens in production. + +14. **Switch reads to local SQLite** and test offline behavior. ## If the App Is Stuck on `Syncing...` diff --git a/skills/powersync/references/powersync-cli.md b/skills/powersync/references/powersync-cli.md index 1c8e6da..14a14a0 100644 --- a/skills/powersync/references/powersync-cli.md +++ b/skills/powersync/references/powersync-cli.md @@ -7,6 +7,20 @@ metadata: # PowerSync CLI +> **Load this when** setting up, deploying, or managing any PowerSync instance (Cloud or self-hosted). This is the primary tool for all PowerSync operations. + +## Table of Contents +- [Recommended Defaults for Agents](#recommended-defaults-for-agents) +- [Installation](#installation) +- [Instance Resolution](#how-the-cli-resolves-instance-information) +- [Authentication](#authentication) +- [Config Files](#config-files) +- [Cloud Usage](#cloud-usage) +- [Self-Hosted Usage](#self-hosted-usage) +- [Docker Commands](#docker-commands-reference) +- [Common Commands](#common-commands) +- [Development Tokens](#development-tokens) + The PowerSync CLI manages Cloud and self-hosted PowerSync instances from the command line. It supports local config management, schema generation, development token generation, deployment, and more. See [this](https://docs.powersync.com/tools/cli.md) for any information not supplied in this document about the CLI. ## Recommended Defaults for Agents diff --git a/skills/powersync/references/powersync-debug.md b/skills/powersync/references/powersync-debug.md index 89350d6..73f20db 100644 --- a/skills/powersync/references/powersync-debug.md +++ b/skills/powersync/references/powersync-debug.md @@ -7,6 +7,8 @@ metadata: # PowerSync Debug +> **Load this when** troubleshooting sync issues, stuck "Syncing..." states, JWT errors, or replication problems. + These are debugging steps most frequently recommended by PowerSync, with an explanation of what problem each step helps identify and why it works. Make sure to understand the [PowerSync Architecture](references/powersync-overview.md) before debugging. diff --git a/skills/powersync/references/powersync-overview.md b/skills/powersync/references/powersync-overview.md index ebc0da3..0f7d121 100644 --- a/skills/powersync/references/powersync-overview.md +++ b/skills/powersync/references/powersync-overview.md @@ -7,6 +7,8 @@ metadata: # PowerSync Architecture Overview +> **Load this when** you need to understand how PowerSync's components fit together before diving into implementation. + Guidance for understanding all the moving components of PowerSync. For information about the vision of PowerSync, see [PowerSync Philosophy](https://docs.powersync.com/intro/powersync-philosophy.md) ## Architecture diff --git a/skills/powersync/references/powersync-service.md b/skills/powersync/references/powersync-service.md index f8096e6..03e2edb 100644 --- a/skills/powersync/references/powersync-service.md +++ b/skills/powersync/references/powersync-service.md @@ -7,6 +7,16 @@ metadata: # PowerSync Service +> **Load this when** configuring the PowerSync service itself — self-hosting, Docker, source database connections, bucket storage, or authentication setup. + +## Table of Contents +- [Sync Config](#sync-config) +- [Service Configuration (Self-hosted)](#service-configuration-self-hosted) +- [PowerSync Cloud Setup](#powersync-cloud-setup) +- [Source Database Setup](#source-database-setup) +- [App Backend](#app-backend) +- [Authentication](#authentication) + Guidance for configuring PowerSync Service, sync config, and database replication. Critical warnings for fast setup: diff --git a/skills/powersync/references/raw-tables.md b/skills/powersync/references/raw-tables.md new file mode 100644 index 0000000..ed287fb --- /dev/null +++ b/skills/powersync/references/raw-tables.md @@ -0,0 +1,282 @@ +--- +name: raw-tables +description: PowerSync Raw Tables — native SQLite tables bypassing JSON views, with multi-SDK examples (JS, Dart, Kotlin, Swift, Rust), triggers, local-only columns, and migration strategies +metadata: + tags: raw-tables, sqlite, advanced, powersync, javascript, dart, kotlin, swift, rust +--- + +# Raw Tables + +> **Load this when** the project needs native SQLite tables (column types, constraints, indexes, generated columns) instead of PowerSync's default JSON-based views. Works across all SDKs except .NET. + +Raw tables let PowerSync sync data directly into native SQLite tables you define, instead of storing data as JSON in `ps_data__` and exposing it via views. This gives full SQLite control and better query performance. See [Raw Tables](https://docs.powersync.com/client-sdks/advanced/raw-tables.md) for the full reference. + +## Table of Contents +- [When to Use](#when-to-use-raw-tables) +- [SDK Availability](#sdk-availability) +- [Defining Raw Tables](#defining-raw-tables) (Inferred vs Explicit) +- [Triggers for Local Writes](#triggers-for-local-writes) (Inferred vs Explicit) +- [Local-Only Columns](#local-only-columns) +- [Migrations](#migrations) +- [Caveats](#caveats) + +**Status:** Experimental — not covered by semver stability guarantees. + +## SDK Availability + +| SDK | Min Version | Package | +|-----|-------------|---------| +| JavaScript (Web) | 1.35.0 | `@powersync/web` | +| JavaScript (React Native) | 1.31.0 | `@powersync/react-native` | +| JavaScript (Node) | 0.18.0 | `@powersync/node` | +| Dart / Flutter | 1.18.0 | `package:powersync` | +| Kotlin | 1.11.0 | `com.powersync:core` | +| Swift | 1.12.0 | `PowerSync` | +| Rust | 0.0.4 | `powersync` | +| .NET | — | **Not yet available** | + +## When to Use Raw Tables + +- Indexes on expressions or `GENERATED` columns (PowerSync's default schema only supports basic column indexes) +- Improved query performance for aggregations (`SUM`, `GROUP BY`) — reads typed columns directly instead of extracting from JSON +- Reduced storage overhead — no JSON object per row +- SQLite constraints (`FOREIGN KEY`, `NOT NULL`, `CHECK`) +- Local-only columns that persist across syncs but never upload + +## Defining Raw Tables + +You must create the actual SQLite table yourself before calling `connect()`: + +```sql +CREATE TABLE IF NOT EXISTS todo_lists ( + id TEXT NOT NULL PRIMARY KEY, + created_by TEXT NOT NULL, + title TEXT NOT NULL, + content TEXT +) STRICT; +``` + +### Inferred Setup (Recommended) + +When the local table structure matches the synced table, the SDK can infer `put`/`delete` statements automatically: + +**JavaScript:** +```ts +const mySchema = new Schema({}); +mySchema.withRawTables({ + todo_lists: { schema: {} } +}); +``` + +**Dart:** +```dart +const schema = Schema([], rawTables: [ + RawTable.inferred(name: 'todo_lists', schema: RawTableSchema()), +]); +``` + +**Kotlin:** +```kotlin +val schema = Schema(listOf( + RawTable(name = "todo_lists", schema = RawTableSchema()) +)) +``` + +**Swift:** +```swift +let lists = RawTable(name: "todo_lists", schema: RawTableSchema()) +let schema = Schema(lists) +``` + +**Rust:** +```rust +let table = RawTable::with_schema("todo_lists", RawTableSchema::default()); +schema.raw_tables.push(table); +``` + +Use inferred setup when the local table directly maps to the synced output table. Use explicit setup (below) for transformations, custom defaults, the `_extra` column pattern, or when local and backend table names differ. + +### Explicit Setup + +Provide `put` and `delete` SQL statements with positional parameters: + +**JavaScript:** +```ts +mySchema.withRawTables({ + todo_lists: { + put: { + sql: 'INSERT OR REPLACE INTO todo_lists (id, created_by, title, content) VALUES (?, ?, ?, ?)', + params: ['Id', { Column: 'created_by' }, { Column: 'title' }, { Column: 'content' }] + }, + delete: { + sql: 'DELETE FROM todo_lists WHERE id = ?', + params: ['Id'] + } + } +}); +``` + +**Dart:** +```dart +RawTable( + name: 'todo_lists', + put: PendingStatement( + sql: 'INSERT OR REPLACE INTO todo_lists (id, created_by, title, content) VALUES (?, ?, ?, ?)', + params: [.id(), .column('created_by'), .column('title'), .column('content')], + ), + delete: PendingStatement(sql: 'DELETE FROM todo_lists WHERE id = ?', params: [.id()]), +) +``` + +**Kotlin:** +```kotlin +RawTable( + name = "todo_lists", + put = PendingStatement( + "INSERT OR REPLACE INTO todo_lists (id, created_by, title, content) VALUES (?, ?, ?, ?)", + listOf(PendingStatementParameter.Id, PendingStatementParameter.Column("created_by"), + PendingStatementParameter.Column("title"), PendingStatementParameter.Column("content")) + ), + delete = PendingStatement("DELETE FROM todo_lists WHERE id = ?", listOf(PendingStatementParameter.Id)) +) +``` + +**Swift:** +```swift +RawTable( + name: "todo_lists", + put: PendingStatement( + sql: "INSERT OR REPLACE INTO todo_lists (id, created_by, title, content) VALUES (?, ?, ?, ?)", + parameters: [.id, .column("created_by"), .column("title"), .column("content")] + ), + delete: PendingStatement(sql: "DELETE FROM todo_lists WHERE id = ?", parameters: [.id]) +) +``` + +Parameter types: `Id` = row ID from sync service, `Column("name")` = column value from synced row, `Rest` = remaining columns as JSON (for the `_extra` pattern). + +## Triggers for Local Writes + +Raw tables need triggers to capture local writes into PowerSync's upload queue (`powersync_crud` virtual table). + +### Inferred Triggers (Recommended) + +Use `powersync_create_raw_table_crud_trigger` — must be called **after** the `CREATE TABLE`: + +**JavaScript:** +```ts +for (const write of ["INSERT", "UPDATE", "DELETE"]) { + await db.execute("SELECT powersync_create_raw_table_crud_trigger(?, ?, ?)", + [JSON.stringify(Schema.rawTableToJson(table)), `todo_lists_${write}`, write]); +} +``` + +**Dart:** +```dart +for (final write in ["INSERT", "UPDATE", "DELETE"]) { + await db.execute("SELECT powersync_create_raw_table_crud_trigger(?, ?, ?)", + [json.encode(table), "todo_lists_$write", write]); +} +``` + +**Kotlin:** +```kotlin +for (write in listOf("INSERT", "UPDATE", "DELETE")) { + database.execute("SELECT powersync_create_raw_table_crud_trigger(?, ?, ?)", + listOf(table.jsonDescription(), "todo_lists_$write", write)) +} +``` + +**Swift:** +```swift +for write in ["INSERT", "UPDATE", "DELETE"] { + try await database.execute( + sql: "SELECT powersync_create_raw_table_crud_trigger(?, ?, ?)", + parameters: [lists.jsonDescription(), "todo_lists_\(write)", write]) +} +``` + +### Explicit Triggers + +Define triggers manually for full control: + +```sql +CREATE TRIGGER todo_lists_insert AFTER INSERT ON todo_lists FOR EACH ROW +BEGIN + INSERT INTO powersync_crud (op, id, type, data) + VALUES ('PUT', NEW.id, 'todo_lists', json_object( + 'created_by', NEW.created_by, 'title', NEW.title, 'content', NEW.content)); +END; + +CREATE TRIGGER todo_lists_update AFTER UPDATE ON todo_lists FOR EACH ROW +BEGIN + SELECT CASE WHEN (OLD.id != NEW.id) THEN RAISE(FAIL, 'Cannot update id') END; + INSERT INTO powersync_crud (op, id, type, data) + VALUES ('PATCH', NEW.id, 'todo_lists', json_object( + 'created_by', NEW.created_by, 'title', NEW.title, 'content', NEW.content)); +END; + +CREATE TRIGGER todo_lists_delete AFTER DELETE ON todo_lists FOR EACH ROW +BEGIN + INSERT INTO powersync_crud (op, id, type) VALUES ('DELETE', OLD.id, 'todo_lists'); +END; +``` + +The `powersync_crud` virtual table columns: `op` (PUT/PATCH/DELETE), `id`, `type` (table name), `data` (JSON), `old_values` (optional), `metadata` (optional). + +## Local-Only Columns + +Raw tables can include columns that exist only on the client — never synced or uploaded. Useful for client preferences, UI state, or local notes. + +Add the column to the table and specify `syncedColumns` in the inferred setup so the SDK knows which columns come from the server: + +**JavaScript:** +```ts +{ name: 'todo_lists', schema: { syncedColumns: ['created_by', 'title', 'content'] } } +``` + +**Dart:** +```dart +RawTableSchema(syncedColumns: ['created_by', 'title', 'content']) +``` + +**Kotlin:** +```kotlin +RawTableSchema(syncedColumns = listOf("created_by", "title", "content")) +``` + +With explicit setup, use `INSERT ... ON CONFLICT(id) DO UPDATE SET` (not `INSERT OR REPLACE`) to avoid resetting local-only columns on sync. Exclude local-only columns from triggers. + +## Migrations + +PowerSync's JSON-based tables need no migrations. Raw tables do — you manage the schema. + +### Adding a new raw table + +If data was already synced before the raw table existed, it's in `ps_untyped`. Copy it after creating the table: + +```sql +INSERT INTO my_table (id, col1, col2) + SELECT id, data ->> 'col1', data ->> 'col2' + FROM ps_untyped WHERE type = 'my_table'; +DELETE FROM ps_untyped WHERE type = 'my_table'; +``` + +Not needed if the raw table was present from the first `connect()` call. + +### Adding columns + +Three strategies: + +1. **Delete and resync:** `disconnectAndClear(soft: true)` → migrate → reconnect. Safest but requires network. +2. **Trigger resync:** `ALTER TABLE ... ADD COLUMN` with a default → `SELECT powersync_trigger_resync(TRUE)`. App stays usable offline with optimistic defaults until resync completes. +3. **`_extra` column pattern:** Store unknown columns as JSON in an `_extra TEXT` column using the `Rest` parameter. Migrate by extracting from `_extra`: `json_extract(_extra, '$.newCol')`. + +## Caveats + +- **Not available on .NET** yet +- **No automatic column migration** — adding columns requires one of the migration strategies above +- **Foreign keys** — must use `DEFERRABLE INITIALLY DEFERRED`; enable with `PRAGMA foreign_keys = ON`; avoid FK references from high-priority to lower-priority raw tables +- **`disconnectAndClear()` won't clear raw tables** by default — add a `clear` statement to `RawTable` if needed +- **Table name** — the `name` property matches the backend table name, not necessarily the local SQLite table name +- **Drop and re-create triggers** after altering a raw table diff --git a/skills/powersync/references/sdks/powersync-dart.md b/skills/powersync/references/sdks/powersync-dart.md index 4f55325..d48c29d 100644 --- a/skills/powersync/references/sdks/powersync-dart.md +++ b/skills/powersync/references/sdks/powersync-dart.md @@ -7,6 +7,8 @@ metadata: # PowerSync Dart SDK +> **Load this when** building a Flutter or Dart app with PowerSync. + Best practices and guidance for building Flutter apps with the PowerSync Dart SDK. | Resource | Description | diff --git a/skills/powersync/references/sdks/powersync-dotnet.md b/skills/powersync/references/sdks/powersync-dotnet.md index 2b1ebb5..e5e9fe2 100644 --- a/skills/powersync/references/sdks/powersync-dotnet.md +++ b/skills/powersync/references/sdks/powersync-dotnet.md @@ -7,6 +7,18 @@ metadata: # PowerSync .NET SDK +> **Load this when** building a .NET app (MAUI, WPF, Console) with PowerSync. + +## Table of Contents +- [Installation](#installation) +- [Quick Setup](#quick-setup) +- [Query Patterns](#query-patterns) +- [Writes and Transactions](#writes-and-transactions) +- [Sync Status](#sync-status) +- [Sync Streams](#sync-streams) +- [Logging](#logging) +- [Schema Updates](#schema-updates) + Best practices for building apps with the PowerSync .NET SDK. Supported targets: .NET 9, .NET 8, .NET 6, .NET Standard 2.0, .NET Framework 4.8. Application frameworks: MAUI (iOS, Android, Windows), WPF, Console. diff --git a/skills/powersync/references/sdks/powersync-js-node.md b/skills/powersync/references/sdks/powersync-js-node.md index ea6efe4..e728e95 100644 --- a/skills/powersync/references/sdks/powersync-js-node.md +++ b/skills/powersync/references/sdks/powersync-js-node.md @@ -7,6 +7,8 @@ metadata: # PowerSync Node.js & Electron +> **Load this when** building a Node.js CLI tool, background sync process, ETL pipeline, or Electron desktop app. Always load `powersync-js.md` first. + Node.js-specific integration for the PowerSync JavaScript SDK. Use this reference alongside `references/sdks/powersync-js.md` when building Node.js CLI tools, background sync processes, ETL pipelines, or Electron desktop apps. | Resource | Description | @@ -19,7 +21,7 @@ Node.js-specific integration for the PowerSync JavaScript SDK. Use this referenc ### 1. Install ```bash -npm install @powersync/node +npm install @powersync/node@latest npm install better-sqlite3 # required peer dependency ``` @@ -123,7 +125,7 @@ Electron App The renderer process is a full browser environment. Set it up exactly like any other web app: ```bash -npm install @powersync/web @journeyapps/wa-sqlite @powersync/react +npm install @powersync/web@latest @journeyapps/wa-sqlite@latest @powersync/react@latest ``` Use `PowerSyncContext.Provider` and `useQuery`/`useStatus` hooks as documented in `references/sdks/powersync-js-react.md`. The Web-Specific Options section in `references/sdks/powersync-js.md` (VFS options, multi-tab, `debugMode`) also applies to the renderer process. @@ -133,7 +135,7 @@ Use `PowerSyncContext.Provider` and `useQuery`/`useStatus` hooks as documented i The main process runs Node.js with no DOM. Use `@powersync/node`: ```bash -npm install @powersync/node better-sqlite3 +npm install @powersync/node@latest better-sqlite3 ``` ```ts diff --git a/skills/powersync/references/sdks/powersync-js-orm.md b/skills/powersync/references/sdks/powersync-js-orm.md new file mode 100644 index 0000000..8eff9be --- /dev/null +++ b/skills/powersync/references/sdks/powersync-js-orm.md @@ -0,0 +1,97 @@ +--- +name: powersync-js-orm +description: Drizzle and Kysely ORM integration with PowerSync JavaScript SDK — type-safe queries, schema definition, and CompilableQuery usage +metadata: + tags: javascript, typescript, drizzle, kysely, orm, powersync +--- + +> **Load this when** the project uses Drizzle or Kysely ORM with PowerSync. Always load `powersync-js.md` first. + +# Drizzle & Kysely ORM Integration + +PowerSync provides official drivers for both Drizzle and Kysely. These let you write type-safe queries that work with PowerSync's reactive hooks (`useQuery`, `useSuspenseQuery`) via the `CompilableQuery` interface. + +| ORM | Package | Docs | +|-----|---------|------| +| Drizzle | `@powersync/drizzle-driver` | [Drizzle Setup](https://docs.powersync.com/client-sdks/orms/javascript-web/drizzle.md) | +| Kysely | `@powersync/kysely-driver` | [Kysely Setup](https://docs.powersync.com/client-sdks/orms/javascript-web/kysely.md) | + +## Drizzle + +```bash +npm install @powersync/drizzle-driver@latest drizzle-orm +``` + +```ts +import { drizzle } from '@powersync/drizzle-driver'; +import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; +import { eq } from 'drizzle-orm'; + +// Define Drizzle schema — separate from the PowerSync schema +export const todos = sqliteTable('todos', { + id: text('id').primaryKey(), + description: text('description').notNull(), + completed: integer('completed').notNull().default(0), + listId: text('list_id') +}); + +// Create Drizzle instance from the PowerSync database +const drizzleDb = drizzle(db); + +// Type-safe queries +const activeTodos = await drizzleDb + .select() + .from(todos) + .where(eq(todos.completed, 0)); +``` + +### Drizzle with useQuery + +Drizzle queries implement `CompilableQuery`, so they work directly with PowerSync hooks: + +```ts +const query = drizzleDb.select().from(todos).where(eq(todos.completed, 0)); +const { data } = useQuery(query); +``` + +## Kysely + +```bash +npm install @powersync/kysely-driver@latest kysely +``` + +```ts +import { wrapPowerSyncWithKysely } from '@powersync/kysely-driver'; + +// Define types for Kysely +interface Database { + todos: { id: string; description: string; completed: number; list_id: string }; + lists: { id: string; name: string; owner_id: string }; +} + +// Create Kysely instance from the PowerSync database +const kyselyDb = wrapPowerSyncWithKysely(db); + +// Type-safe queries +const activeTodos = await kyselyDb + .selectFrom('todos') + .selectAll() + .where('completed', '=', 0) + .execute(); +``` + +### Kysely with useQuery + +```ts +const query = kyselyDb.selectFrom('todos').selectAll().where('completed', '=', 0); +const { data } = useQuery(query); +``` + +## Both schemas are needed + +You need **two** schemas when using an ORM: + +1. **PowerSync schema** (`new Schema(...)`) — defines what data syncs. Required. +2. **ORM schema** (Drizzle `sqliteTable` or Kysely interface) — provides type safety for queries. Optional but recommended. + +These must stay in sync — if you add a column to the PowerSync schema, add it to the ORM schema too. diff --git a/skills/powersync/references/sdks/powersync-js-react-native.md b/skills/powersync/references/sdks/powersync-js-react-native.md index 5b57db4..aa556db 100644 --- a/skills/powersync/references/sdks/powersync-js-react-native.md +++ b/skills/powersync/references/sdks/powersync-js-react-native.md @@ -7,6 +7,8 @@ metadata: # PowerSync React Native, Expo & Expo Go +> **Load this when** building a React Native app, Expo app (managed or bare), or Expo Go sandbox. Always load `powersync-js.md` first. + React Native-specific integration for the PowerSync JavaScript SDK. Use this reference alongside `references/sdks/powersync-js.md` when building React Native apps, Expo apps (managed or bare workflow), or Expo Go sandboxes. The React hooks API (`useQuery`, `useStatus`, `usePowerSync`, `useSuspenseQuery`) from `@powersync/react-native` is identical to `@powersync/react` — see `references/sdks/powersync-js.md` for full hook patterns and `references/sdks/powersync-js-react.md` for `useSuspenseQuery` and sync stream hooks. @@ -22,17 +24,17 @@ The React hooks API (`useQuery`, `useStatus`, `usePowerSync`, `useSuspenseQuery` ### Standard React Native (Recommended) ```bash -npm install @powersync/react-native +npm install @powersync/react-native@latest ``` Then install a native SQLite adapter (required peer dependency): ```bash # OP-SQLite — recommended: built-in encryption, React Native New Architecture support -npm install @powersync/op-sqlite +npm install @powersync/op-sqlite@latest # OR: React Native Quick SQLite — original adapter -npm install @journeyapps/react-native-quick-sqlite +npm install @journeyapps/react-native-quick-sqlite@latest ``` After installing native dependencies, rebuild your native app: @@ -108,7 +110,7 @@ Expo Go is a sandbox that does not support native modules. To run PowerSync in E ### Install ```bash -npm install @powersync/react-native @powersync/adapter-sql-js +npm install @powersync/react-native@latest @powersync/adapter-sql-js@latest ``` ### Usage diff --git a/skills/powersync/references/sdks/powersync-js-react.md b/skills/powersync/references/sdks/powersync-js-react.md index e31f463..0f866e2 100644 --- a/skills/powersync/references/sdks/powersync-js-react.md +++ b/skills/powersync/references/sdks/powersync-js-react.md @@ -7,6 +7,16 @@ metadata: # PowerSync React & Next.js +> **Load this when** building a React web app, Next.js app, or any Vite + React project. Load **before** package install for Vite projects — contains the required `vite.config.ts` setup. Always load `powersync-js.md` first. + +## Table of Contents +- [Provider Setup](#provider-setup) +- [useSuspenseQuery](#usesuspensequery) +- [Sync Stream Hooks](#sync-stream-hooks) +- [Next.js Setup](#nextjs-setup) +- [Vite Setup](#vite-setup) +- [Common Pitfalls](#common-pitfalls) + React-specific integration for the PowerSync JavaScript SDK. Use this reference alongside `references/sdks/powersync-js.md` when building React web apps, Next.js apps, or when using the React hooks from `@powersync/react` or `@powersync/react-native`. | Resource | Description | @@ -124,7 +134,7 @@ PowerSync is tailored for client-side applications. Next.js evaluates code in a ### Install ```bash -npm install @powersync/web @journeyapps/wa-sqlite @powersync/react +npm install @powersync/web@latest @journeyapps/wa-sqlite@latest @powersync/react@latest ``` ### Copy Worker Assets (Turbopack) diff --git a/skills/powersync/references/sdks/powersync-js-tanstack.md b/skills/powersync/references/sdks/powersync-js-tanstack.md index 6ed4ba2..983a2a0 100644 --- a/skills/powersync/references/sdks/powersync-js-tanstack.md +++ b/skills/powersync/references/sdks/powersync-js-tanstack.md @@ -7,6 +7,8 @@ metadata: # PowerSync TanStack Integrations +> **Load this when** using TanStack Query (React) or TanStack DB (multi-framework) with PowerSync. Always load `powersync-js.md` first. + TanStack-specific integrations for the PowerSync JavaScript SDK. Use this reference alongside `references/sdks/powersync-js.md` when using TanStack Query (React) or TanStack DB (multi-framework) with PowerSync. | Resource | Description | @@ -35,7 +37,7 @@ Use `@powersync/react` hooks (from `references/sdks/powersync-js-react.md`) when ### Install ```bash -npm install @powersync/tanstack-react-query +npm install @powersync/tanstack-react-query@latest ``` TanStack Query also requires its core peer: @@ -107,10 +109,10 @@ Use plain PowerSync (`.watch()` / `usePowerSyncQuery`) when: ```bash # Web -npm install @tanstack/powersync-db-collection @powersync/web @journeyapps/wa-sqlite +npm install @tanstack/powersync-db-collection@latest @powersync/web@latest @journeyapps/wa-sqlite@latest # React Native -npm install @tanstack/powersync-db-collection @powersync/react-native +npm install @tanstack/powersync-db-collection@latest @powersync/react-native@latest ``` Also install a TanStack DB framework adapter for your UI framework: diff --git a/skills/powersync/references/sdks/powersync-js-vue.md b/skills/powersync/references/sdks/powersync-js-vue.md index d02c80d..1588437 100644 --- a/skills/powersync/references/sdks/powersync-js-vue.md +++ b/skills/powersync/references/sdks/powersync-js-vue.md @@ -7,6 +7,12 @@ metadata: # PowerSync Vue & Nuxt +> **Load this when** building a Vue app or Nuxt app with PowerSync. Always load `powersync-js.md` first. + +## Table of Contents +- [Vue](#vue) (Plugin, Composables, Watchers) +- [Nuxt](#nuxt) (Module, Auto-imports, SSR) + Vue-specific integration for the PowerSync JavaScript SDK. Use this reference alongside `references/sdks/powersync-js.md` when building Vue apps or Nuxt apps. | Resource | Description | @@ -22,10 +28,10 @@ Vue-specific integration for the PowerSync JavaScript SDK. Use this reference al ```bash # npm (v7+ installs peer dependencies automatically) -npm install @powersync/vue +npm install @powersync/vue@latest # pnpm (must install peer dependencies explicitly) -pnpm add @powersync/vue @powersync/web @journeyapps/wa-sqlite +pnpm add @powersync/vue@latest @powersync/web@latest @journeyapps/wa-sqlite@latest ``` ### 2. Plugin Setup @@ -171,10 +177,10 @@ PowerSync is tailored for client-side applications. Nuxt evaluates plugins serve ```bash # npm (v7+ installs peer dependencies automatically) -npm install @powersync/nuxt +npm install @powersync/nuxt@latest # pnpm (must install peer dependencies explicitly) -pnpm add @powersync/nuxt @powersync/vue @powersync/web +pnpm add @powersync/nuxt@latest @powersync/vue@latest @powersync/web@latest ``` ### 2. `nuxt.config.ts` @@ -247,7 +253,7 @@ The module optionally exposes a `usePowerSyncKysely()` composable for type-safe Install the driver: ```bash -npm install @powersync/kysely-driver +npm install @powersync/kysely-driver@latest ``` Enable it in `nuxt.config.ts`: diff --git a/skills/powersync/references/sdks/powersync-js.md b/skills/powersync/references/sdks/powersync-js.md index 84baf5c..e15c505 100644 --- a/skills/powersync/references/sdks/powersync-js.md +++ b/skills/powersync/references/sdks/powersync-js.md @@ -1,13 +1,31 @@ --- name: powersync-js -description: PowerSync JavaScript/TypeScript SDK — schema, backend connector, queries, transactions, sync status, raw tables, Drizzle/Kysely ORM, and debugging +description: PowerSync JavaScript/TypeScript SDK — schema, backend connector, queries, transactions, sync status, and debugging metadata: - tags: javascript, typescript, web, sqlite, offline-first, drizzle, kysely + tags: javascript, typescript, web, sqlite, offline-first --- +> **Load this when** working on any JavaScript or TypeScript project with PowerSync. This is the foundation file — always load it first, then load the applicable framework-specific file alongside it. + # PowerSync JavaScript/TypeScript SDK -Core patterns and guidance shared across all PowerSync JavaScript/TypeScript targets. Use this reference for any JS/TS project — it covers schema design, the backend connector, database initialization, transactions, imperative queries, sync status, raw tables, and debugging. Always load this file as the foundation, then load the applicable framework-specific file alongside it. +Core patterns and guidance shared across all PowerSync JavaScript/TypeScript targets — schema design, the backend connector, database initialization, transactions, imperative queries, sync status, and debugging. For ORM integration see `powersync-js-orm.md`; for raw tables and sync internals see `powersync-js-raw-tables.md`. + +## Table of Contents + +- [Package Coverage](#package-coverage) +- [Quick Setup](#quick-setup) (Install, Schema, Backend Connector, uploadData, Initialize) +- [Query Patterns](#query-patterns) (useQuery, CompilableQuery, Imperative, Watch) +- [Writes & Transactions](#writes--transactions) +- [Sync Status, Priorities & Sync Streams](#sync-status-priorities--sync-streams) +- [Debugging](#debugging) +- [Common Pitfalls](#common-pitfalls) + +**TypeScript-only exports:** `PowerSyncBackendConnector`, `PowerSyncCredentials`, and `AbstractPowerSyncDatabase` are **TypeScript interfaces only** — they exist at compile time for type checking but have no runtime presence. This means: +- Always use `import type` for these (e.g. `import type { PowerSyncBackendConnector } from '@powersync/web'`) +- Runtime checks like `require('@powersync/web').PowerSyncBackendConnector` will return `undefined` — this is expected, not a bug +- Only `UpdateType` is a runtime value (enum) and uses a regular import +- Bundlers like Vite will error if you import these without `type` since they try to resolve them as values | Resource | Description | |----------|-------------| @@ -52,34 +70,34 @@ Framework-specific files (load alongside this file): ```bash # Web -npm install @powersync/web -npm install @journeyapps/wa-sqlite # Needed (peer-dependency) +npm install @powersync/web@latest +npm install @journeyapps/wa-sqlite@latest # Needed (peer-dependency) # React Native -npm install @powersync/react-native -npm install @powersync/powersync-op-sqlite # Needed (peer-dependency) +npm install @powersync/react-native@latest +npm install @powersync/powersync-op-sqlite@latest # Needed (peer-dependency) # Node.js -npm install @powersync/node +npm install @powersync/node@latest npm install better-sqlite3 # Needed (peer-dependency) # React integration -npm install @powersync/react +npm install @powersync/react@latest # Vue -npm install @powersync/vue +npm install @powersync/vue@latest # Nuxt (includes @powersync/vue — npm v7+ installs peers automatically) -npm install @powersync/nuxt +npm install @powersync/nuxt@latest # TanStack Query (React) -npm install @powersync/tanstack-react-query +npm install @powersync/tanstack-react-query@latest # TanStack DB -npm install @tanstack/powersync-db-collection +npm install @tanstack/powersync-db-collection@latest ``` -Always install packages by running these commands rather than writing versions into `package.json` manually. Using `"latest"` as a version string in `package.json` is incorrect — it bypasses the lockfile and can pull in breaking changes at any install. +Always install packages using `@latest` as shown above — PowerSync releases frequently and older cached versions can be missing critical fixes. Do not write version strings into `package.json` manually. See the framework-specific files for full setup instructions per target. @@ -142,8 +160,6 @@ const tasks = new Table( See [Integrate with your Backend](https://docs.powersync.com/client-sdks/reference/javascript-web.md#3-integrate-with-your-backend) and [Client-Side Integration](https://docs.powersync.com/configuration/app-backend/client-side-integration.md) for more information. -**IMPORTANT:** `PowerSyncBackendConnector`, `PowerSyncCredentials`, and `AbstractPowerSyncDatabase` are **type-only exports**. Always use `import type` for these — importing them as values (without `type`) causes runtime errors in bundlers like Vite. Only `UpdateType` is a runtime value (enum) and uses a regular import. - ```ts import type { PowerSyncBackendConnector, PowerSyncCredentials } from '@powersync/web' async fetchCredentials(): Promise { @@ -668,157 +684,31 @@ subscription.unsubscribe(); - Default streams: server may configure streams as default — these subscribe automatically without a client call - TTL eviction: after TTL expires with no active subscriber, the stream's data may be removed from the local DB -## Raw Tables - -Raw tables let PowerSync sync data directly into native SQLite tables you define, instead of storing data as JSON in `ps_data__
` and exposing it via views. This gives full SQLite control and better query performance. See [Raw Tables](https://docs.powersync.com/usage/use-case-examples/raw-tables.md) for more information. - -Requires: The Rust sync client (now the default). Will not work with the legacy JavaScript client. - -Status: Experimental — not covered by semver stability guarantees. - -### When to Use Raw Tables - -- Complex queries that benefit from native column types (e.g. `SUM`, `GROUP BY` on typed columns) -- Tables with many rows where JSON extraction overhead is significant -- Need for SQLite constraints (foreign keys, `NOT NULL`, `GENERATED` columns) -- Custom indexes on expressions or generated columns - -### Defining a Raw Table - -```ts -import { Schema, RawTable } from '@powersync/common'; - -const schema = new Schema({ - // Regular PowerSync-managed tables here -}); - -schema.withRawTables({ - // The key name ('todo_lists') matches the table name in the backend database - // as sent by the PowerSync service — NOT necessarily the local SQLite table name - todo_lists: { - put: { - sql: 'INSERT OR REPLACE INTO todo_lists (id, created_by, title, content) VALUES (?, ?, ?, ?)', - params: ['Id', { Column: 'created_by' }, { Column: 'title' }, { Column: 'content' }] - }, - delete: { - sql: 'DELETE FROM todo_lists WHERE id = ?', - params: ['Id'] - } - } -}); -``` - -Parameter types: -- `'Id'` — replaced with the object ID from the sync service -- `{ Column: 'fieldName' }` — replaced with the value of that column from the synced row data -- For `delete` statements, only `'Id'` is supported - -You must also create the actual SQLite table separately (e.g. in app init): - -```ts -await db.execute(` - CREATE TABLE IF NOT EXISTS todo_lists ( - id TEXT NOT NULL PRIMARY KEY, - created_by TEXT NOT NULL, - title TEXT NOT NULL, - content TEXT - ) STRICT -`); -``` - -### Triggers for Local Writes - -Raw tables require manual triggers to capture local writes into PowerSync's upload queue (`powersync_crud` virtual table): - -```sql -CREATE TRIGGER todo_lists_insert - AFTER INSERT ON todo_lists FOR EACH ROW - BEGIN - INSERT INTO powersync_crud (op, id, type, data) - VALUES ('PUT', NEW.id, 'todo_lists', json_object( - 'created_by', NEW.created_by, - 'title', NEW.title, - 'content', NEW.content - )); - END; - -CREATE TRIGGER todo_lists_update - AFTER UPDATE ON todo_lists FOR EACH ROW - BEGIN - SELECT CASE - WHEN (OLD.id != NEW.id) THEN RAISE(FAIL, 'Cannot update id') - END; - INSERT INTO powersync_crud (op, id, type, data) - VALUES ('PATCH', NEW.id, 'todo_lists', json_object( - 'created_by', NEW.created_by, - 'title', NEW.title, - 'content', NEW.content - )); - END; - -CREATE TRIGGER todo_lists_delete - AFTER DELETE ON todo_lists FOR EACH ROW - BEGIN - INSERT INTO powersync_crud (op, id, type) - VALUES ('DELETE', OLD.id, 'todo_lists'); - END; -``` - -The `powersync_crud` virtual table fields: -- `op` — `'PUT'`, `'PATCH'`, or `'DELETE'` -- `id` — row ID -- `type` — table name (as the backend knows it) -- `data` — JSON object of column values (omit for DELETE) -- `old_values` — optional previous values for conflict resolution -- `metadata` — optional metadata string +## ORM & Raw Tables -### Migrating from ps_untyped +These advanced topics are in separate files — load only when needed: -If PowerSync has already synced data for a table before you added it as a raw table, it's stored in `ps_untyped`. After creating the raw table and defining it in the schema, run: +| Topic | File | Load when… | +|-------|------|-----------| +| Drizzle / Kysely ORM | `references/sdks/powersync-js-orm.md` | Using Drizzle or Kysely for type-safe queries | +| Raw Tables | `references/raw-tables.md` | Need native SQLite tables (SDK-agnostic — JS, Dart, Kotlin, Swift, Rust) | -```sql -INSERT INTO my_table (id, col1, col2) - SELECT id, data ->> 'col1', data ->> 'col2' - FROM ps_untyped WHERE type = 'my_table'; -DELETE FROM ps_untyped WHERE type = 'my_table'; -``` - -Not needed if the raw table definition was present from the very first `connect()` call. +## JS Internals -### Raw Table Caveats +> Only needed when debugging QueryStore eviction, investigating sync client implementations, or working with internal op types. -- Rust client only — the JavaScript sync client logs a warning and ignores raw tables -- No automatic column migration — adding columns requires deleting all data and resyncing, or a manual workaround -- Foreign keys — must use `DEFERRABLE INITIALLY DEFERRED`; enable with `PRAGMA foreign_keys = ON`; avoid FK references from high-priority to lower-priority raw tables (priorities sync in separate transactions) -- `disconnectAndClear()` won't clear raw tables by default — add a `clear` statement to `RawTable` if needed -- The `name` property matches the backend table name, not the local SQLite table name — `put`/`delete` can target any local table +### Sync Client Implementations -## Drizzle ORM Integration +The Rust-based sync client is now the default — no config needed. The legacy JS client (`SyncClientImplementation.JAVASCRIPT`) is deprecated. Do not downgrade the SDK after using the Rust client — older JS client versions can't read the Rust format. -See [Drizzle ORM Setup](https://docs.powersync.com/client-sdks/orms/javascript-web/drizzle.md) for full setup instructions. +### QueryStore -```ts -import { drizzle } from '@powersync/drizzle-driver'; -import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; -import { eq } from 'drizzle-orm'; - -// Define Drizzle schema -export const todos = sqliteTable('todos', { - id: text('id').primaryKey(), - description: text('description').notNull(), - completed: integer('completed').notNull().default(0), - listId: text('list_id') -}); +`useSuspenseQuery` uses a `QueryStore` (one per `PowerSyncDatabase`, stored in a `WeakMap`). Caches `WatchedQuery` instances keyed by `"${sql} -- ${JSON.stringify(params)} -- ${JSON.stringify(options)}"`. Evicted when listener count reaches 0. `useSuspenseQuery` and `useQuery` with the same SQL/params/options share the same underlying `WatchedQuery`. -// Create Drizzle instance -const drizzleDb = drizzle(db); +### Op Types (Internal Sync vs CRUD) -// Type-safe queries -const activeTodos = await drizzleDb - .select() - .from(todos) - .where(eq(todos.completed, 0)); -``` +Internal bucket ops (`OpTypeEnum`): `CLEAR=1`, `MOVE=2`, `PUT=3`, `REMOVE=4` — sync protocol only, not exposed to userland. +CRUD upload ops (`UpdateType`): `PUT`, `PATCH`, `DELETE` — what you see in `uploadData`. Don't confuse sync-level `REMOVE` with CRUD-level `DELETE`. ## Debugging @@ -925,58 +815,6 @@ console.log(db.currentStatus); // { connected, connecting, lastSyncedAt, hasSynced, isSyncing, downloadProgress } ``` -## Internals - -> Load this section only when debugging QueryStore eviction behaviour, investigating sync client implementation differences, or working with internal op types. Not needed for typical integration, setup, or feature work. - -### Sync Client Implementations - -The Rust-based sync client is now the default: - -```ts -// Default — Rust client (no config needed) -const db = new PowerSyncDatabase({ schema, database: { dbFilename: 'app.db' } }); - -// Explicit (not needed unless reverting to legacy) -import { SyncClientImplementation } from '@powersync/common'; -const db = new PowerSyncDatabase({ - schema, - database: { dbFilename: 'app.db' }, - sync: { implementation: SyncClientImplementation.RUST } // now default -}); -``` - -The Rust client: -- More performant — offloads sync line decoding to native extension -- Required for raw tables and partial checkpoints by priority -- Stores sync data in a slightly different format than the old JS client -- Auto-migrates from JS format on first use - -Do not downgrade the SDK after using the Rust client — older SDK versions using the JS client can't read the Rust format. - -The legacy JS client (`SyncClientImplementation.JAVASCRIPT`) is deprecated and will be removed in a future version. - -### QueryStore - -`useSuspenseQuery` uses a `QueryStore` (one per `PowerSyncDatabase` instance, stored in a `WeakMap`). The store caches `WatchedQuery` instances keyed by: - -``` -"${sql} -- ${JSON.stringify(params)} -- ${JSON.stringify(options)}" -``` - -A query is evicted (closed) when the count of `ON_DATA + ON_STATE_CHANGE + ON_ERROR` listeners reaches 0. - -Implication: `useSuspenseQuery` and `useQuery` with the same SQL/params/options share the same underlying `WatchedQuery`. If one component unmounts but another with the same query is still mounted, the query stays alive and is not re-fetched. - -### Op Types (Internal Sync vs CRUD) - -Internal bucket ops (`OpTypeEnum`) — used inside sync protocol, not exposed to userland: -- `CLEAR=1`, `MOVE=2`, `PUT=3`, `REMOVE=4` - -CRUD upload ops (`UpdateType`) — what you see in `uploadData`: -- `PUT`, `PATCH`, `DELETE` - -These are separate enumerations. Don't confuse the sync-level `REMOVE` with the CRUD-level `DELETE`. ## Common Pitfalls diff --git a/skills/powersync/references/sdks/powersync-kotlin.md b/skills/powersync/references/sdks/powersync-kotlin.md index 3f544df..1247c84 100644 --- a/skills/powersync/references/sdks/powersync-kotlin.md +++ b/skills/powersync/references/sdks/powersync-kotlin.md @@ -7,6 +7,18 @@ metadata: # PowerSync Kotlin SDK +> **Load this when** building a Kotlin app (Android, JVM, KMP) with PowerSync. + +## Table of Contents +- [Installation](#installation) +- [Quick Setup](#quick-setup) +- [Query Patterns](#query-patterns) +- [Writes and Transactions](#writes-and-transactions) +- [Compose Integration](#compose-integration) +- [Sync Status](#sync-status) +- [Sync Streams](#sync-streams) +- [Background Sync (Android)](#background-sync-android) + Best practices for building apps with the PowerSync Kotlin SDK. Supported targets: Android, JVM, iOS, macOS, watchOS, tvOS. diff --git a/skills/powersync/references/sdks/powersync-swift.md b/skills/powersync/references/sdks/powersync-swift.md index 4e9455e..4ad8628 100644 --- a/skills/powersync/references/sdks/powersync-swift.md +++ b/skills/powersync/references/sdks/powersync-swift.md @@ -7,6 +7,8 @@ metadata: # PowerSync Swift SDK +> **Load this when** building a Swift app (iOS, macOS) with PowerSync. + Best practices and guidance for building apps with the PowerSync Swift SDK. | Resource | Description | diff --git a/skills/powersync/references/supabase-auth.md b/skills/powersync/references/supabase-auth.md index de081b4..a411fdf 100644 --- a/skills/powersync/references/supabase-auth.md +++ b/skills/powersync/references/supabase-auth.md @@ -7,6 +7,16 @@ metadata: # PowerSync + Supabase Auth +> **Load this when** using Supabase as the backend — covers database publication setup, JWT signing keys, fetchCredentials(), uploadData error handling, and Cloud/self-hosted auth config. + +## Table of Contents +- [Supabase Database Setup](#supabase-database-setup) +- [JWT Signing Key Types](#jwt-signing-key-types) +- [PowerSync Cloud Setup](#powersync-cloud-setup) +- [Self-Hosted Config](#self-hosted-serviceyaml-config) +- [fetchCredentials()](#fetchcredentials--client-implementation) +- [Troubleshooting](#troubleshooting) + PowerSync verifies Supabase JWTs directly when connected to a Supabase-hosted Postgres database. This file covers everything needed to configure authentication end-to-end. ## Supabase Database Setup @@ -162,10 +172,18 @@ client_auth: ## `fetchCredentials()` — Client Implementation +**Prerequisite:** `fetchCredentials()` requires an active Supabase auth session. PowerSync calls it automatically whenever a token is needed, but if no session exists (user not signed in), it will throw and sync will not start. **You must sign the user in before calling `db.connect()`.** + +- If your app requires explicit sign-in (email/password, OAuth, magic link), connect PowerSync only after the sign-in completes. +- If anonymous access is acceptable, use the anonymous sign-in pattern below. +- If anonymous auth is disabled on your Supabase project, there is no silent fallback — the agent must gate `db.connect()` behind an explicit auth flow. + `fetchCredentials()` in your backend connector should return the Supabase session JWT. The examples below use the JS Supabase client; equivalent patterns exist for [Dart](https://github.com/powersync-ja/powersync.dart/blob/9ef224175c8969f5602c140bcec6dd8296c31260/demos/supabase-todolist/lib/powersync.dart#L38) and [Kotlin](https://github.com/powersync-ja/powersync-kotlin/blob/main/connectors/supabase/src/commonMain/kotlin/com/powersync/connector/supabase/SupabaseConnector.kt). ### Standard Supabase Auth (JS/TS) +Use this when users sign in explicitly (email, OAuth, magic link). Call `db.connect(connector)` only after `supabase.auth.signIn*` succeeds. + ```ts import { createClient } from '@supabase/supabase-js'; import type { PowerSyncBackendConnector, PowerSyncCredentials } from '@powersync/web'; // or @powersync/react-native @@ -188,13 +206,14 @@ export const connector: PowerSyncBackendConnector = { ### Anonymous Sign-In (JS/TS) +Use this when you want sync to work without an explicit sign-in step. Requires **anonymous sign-ins to be enabled** in Supabase (Dashboard → Authentication → Providers → Anonymous). If disabled, `signInAnonymously()` returns an error and sync fails silently. + ```ts async fetchCredentials(): Promise { - // Sign in anonymously if no session exists let { data: { session } } = await supabase.auth.getSession(); if (!session) { const { data, error } = await supabase.auth.signInAnonymously(); - if (error) throw error; + if (error) throw error; // Will throw if anonymous auth is disabled session = data.session!; } return { @@ -209,10 +228,20 @@ async fetchCredentials(): Promise { ### `uploadData()` — Writing Changes Back to Supabase -For Supabase backends, `uploadData` writes client-side changes directly to Supabase using the Supabase JS client. **`transaction.complete()` is mandatory** — without it the upload queue stalls permanently. +For Supabase backends, `uploadData` writes client-side changes directly to Supabase. **`transaction.complete()` is mandatory** — without it the upload queue stalls permanently. + +#### Error handling strategy + +| Error type | What to do | Why | +|-----------|-----------|-----| +| Network / 5xx (transient) | `throw error` — do not call `transaction.complete()` | PowerSync retries with backoff | +| 4xx / RLS violation (permanent) | Call `transaction.complete()`, log the error | 4xx blocks the queue forever; better to skip and log than halt all future writes | +| Validation error | Call `transaction.complete()`, surface via a synced error table | Data errors are permanent; retrying won't fix them | + +The Supabase JS client returns errors as `{ error: PostgrestError }` rather than throwing HTTP status codes — check `error.code` or `error.message` to distinguish permanent failures (constraint violations, RLS denials) from transient ones. Supabase RLS errors return `{ code: '42501' }` (PostgreSQL insufficient_privilege). ```ts -import type { AbstractPowerSyncDatabase, PowerSyncBackendConnector, CrudEntry, UpdateType } from '@powersync/web'; +import type { AbstractPowerSyncDatabase, PowerSyncBackendConnector, UpdateType } from '@powersync/web'; export const connector: PowerSyncBackendConnector = { async fetchCredentials() { /* ... see above ... */ }, @@ -224,29 +253,34 @@ export const connector: PowerSyncBackendConnector = { try { for (const op of transaction.crud) { const { op: opType, table, opData, id } = op; + let result: { error: any }; if (opType === UpdateType.PUT) { - const { error } = await supabase.from(table).upsert({ ...opData, id }); - if (error) throw error; + result = await supabase.from(table).upsert({ ...opData, id }); } else if (opType === UpdateType.PATCH) { - const { error } = await supabase.from(table).update(opData).eq('id', id); - if (error) throw error; - } else if (opType === UpdateType.DELETE) { - const { error } = await supabase.from(table).delete().eq('id', id); - if (error) throw error; + result = await supabase.from(table).update(opData).eq('id', id); + } else { + result = await supabase.from(table).delete().eq('id', id); } + if (result.error) throw result.error; } - await transaction.complete(); // REQUIRED — clears the queue entry - } catch (error) { - // For 4xx errors (permanent failures), complete the transaction to avoid - // blocking the queue. For 5xx/network errors, throw to trigger a retry. - console.error('Upload error', error); + await transaction.complete(); // REQUIRED — advances the queue + } catch (error: any) { + // Permanent failures (RLS violation, constraint error, 4xx-equivalent): + // complete the transaction so the queue can advance. Log for debugging. + const isPermanent = error?.code === '42501' || error?.status === 400; + if (isPermanent) { + console.error('Permanent upload error, skipping:', error); + await transaction.complete(); + return; + } + // Transient failures: throw so PowerSync retries with backoff. throw error; } } }; ``` -**Important:** RLS policies on your Supabase tables must allow the authenticated user to write their own rows. If `uploadData` consistently gets 4xx errors, the queue stalls — call `transaction.complete()` and log the error rather than retrying forever. +**Important:** RLS policies on your Supabase tables must allow the authenticated user to write their own rows. Ensure `INSERT`/`UPDATE`/`DELETE` policies exist — `SELECT`-only policies silently block all writes. ### Getting the PowerSync Instance URL @@ -305,6 +339,8 @@ PowerSync cannot verify the JWT signature. Check the error logs for `Known keys` | Wrong JWT secret | HS256 verification fails | For legacy HS256 keys, verify the secret matches Supabase → Project Settings → JWT. | | `block_local_jwks` blocking JWKS fetch | JWKS URI resolves to private IP, keys never fetched | Set `block_local_jwks: false` for local development. | +After any `service.yaml` auth change, restart the service to pick it up: `powersync docker reset` (self-hosted) or `powersync deploy service-config` (Cloud). + ### `PSYNC_S2105` — JWT payload is missing a required claim "aud" Using manual JWKS config without specifying an audience. Add `authenticated` to the audience list (Cloud dashboard or `audience: [authenticated]` in `service.yaml`). diff --git a/skills/powersync/references/sync-config.md b/skills/powersync/references/sync-config.md index 4fcc17f..0c8ef36 100644 --- a/skills/powersync/references/sync-config.md +++ b/skills/powersync/references/sync-config.md @@ -7,6 +7,12 @@ metadata: # Sync Config +> **Load this when** writing or modifying sync configuration — Sync Streams (new) or Sync Rules (legacy). Required for every PowerSync project. + +## Table of Contents +**Sync Streams (new):** [Requirements](#requirements) · [File Format](#sync-configyaml-file-format) · [Structure](#structure) · [Stream Options](#stream-options) · [Common Patterns](#common-patterns) · [Query Parameters](#query-parameters) · [CTEs](#common-table-expressions-ctes) · [Migration](#migration) · [Client Usage](#client-usage) · [Advanced Topics](#advanced-topics) +**Sync Rules (legacy):** [Structure](#structure-1) · [Parameter Queries](#parameter-queries) · [Data Queries](#data-queries) · [Supported SQL](#supported-sql-features) · [Common Patterns](#common-patterns-1) + Expert guidance on Sync Config. Sync config is divided into two sections: 1. Sync Streams (new, default) - The latest implementation of Sync Config. New apps should use Sync Streams by default. Prioritize Sync Streams above Sync Rules. 2. Sync Rules (legacy) - The first implementation of Sync Config. New apps should not use Sync Rules, prioritize Sync Streams over Sync Rules.