skills/powersync/references/powersync-debug.md

13 KiB
Raw Permalink Blame History

name description metadata
powersync-debug PowerSync debugging and troubleshooting — sync status, JWT verification, PSYNC error codes, replication lag, and diagnostics tools
tags
debugging, troubleshooting, sync-status, jwt, psync-errors, replication-lag, ps-crud, diagnostics

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 before debugging.

First Response When the UI Is Stuck on Syncing...

Before asking for console logs or editing app code, verify these in order:

  1. The PowerSync endpoint URL returned by fetchCredentials() is correct (not the backend URL).
  2. The PowerSync service has a valid source DB connection.
  3. Sync config was deployed and starts with config: edition: 3.
  4. Client auth is configured correctly (Supabase auth, custom JWKS, or other provider).
  5. Source database replication/publication/CDC is set up for the synced tables.

Only inspect frontend connector code or SDK state after all five checks pass.

Before requesting browser console logs, ask the operator to confirm:

  • the instance exists
  • the DB connection was configured
  • sync config was deployed
  • client auth was configured
  • source database replication/publication/CDC was set up

Check SyncStatus / currentStatus Before Investigating Further

What it identifies: Whether the SDK is actually connected, syncing, or has hit an error, before diving into logs.

Why: SyncStatus is the SDK's live view of its own state. It surfaces connection state, whether a first sync has completed, whether uploads are processing etc. Checking it first avoids chasing a perceived bug that is actually just "not yet connected."

How:

Each of the PowerSync Client SDKs have the SyncStatus class that can be used to access the client sync status.

SDK Link
Flutter SyncStatus Class
Kotlin SyncStatus Class
Swift SyncStatusData
Web SyncStatus Class
React Native SyncStatus Class
Node.js SyncStatus Class
.NET (Alpha) SyncStatus.cs

Key fields to check: connected, downloading, uploading, lastSyncedAt, hasSynced, downloadError, uploadError.

Enable the Request Logger (Swift SDK)

What it identifies: The exact HTTP request being made to the PowerSync Service to inspect the URL, method, headers, authorization token, and response status code. Use this when authentication failures, JWT errors, or endpoint misconfigurations need to be diagnosed on iOS/macOS.

Why: The Rust-based sync client doesn't log request details by default. Adding a SyncRequestLoggerConfiguration gives a full audit trail of every sync stream request — endpoint URL, JWT, response status, and error bodies (including PSYNC_S2101 JWT key-id mismatches).

How:

try await db.connect(
    connector: connector,
    options: ConnectOptions(
        clientConfiguration: SyncClientConfiguration(
            requestLogger: SyncRequestLoggerConfiguration(
                requestLevel: .headers
            ) { message in
                print("[SyncRequest] \(message)")
            }
        )
    )
)

Look for: The Authorization: Token <jwt> header and the response status (200, 401, 404). A 401 with PSYNC_S2101 in the response body means JWT key ID mismatch.

Use the Sync Diagnostics Client

What it identifies: Whether the PowerSync Service is processing your sync rules correctly, what data is in each bucket for a given user, and whether parameter queries are resolving as expected.

Why: Most "data not showing up on the client" issues are actually server-side: wrong Sync Rules / Sync Streams, parameter query not matching etc. The Diagnostics Client lets you verify this without touching client code. It runs entirely in the browser against your real instance.

How: Go to diagnostics-app.powersync.com, connect to your PowerSync instance, and:

  1. Check the Client Parameters page to configure client parameters to test buckets that use parameter queries.
  2. Run queries directly in the SQL Console to confirm row counts.
  3. Check bucket and Sync Streams subscriptions contents to see exactly what data will be synced to a given user.
  4. Use the SQLite File Inspector to drag-and-drop a local .db file and inspect its contents directly in the browser.

The Sync Diagnostics Client is also self-hostable and the Docker image is available on Docker hub.

Inspect ps_crud Directly

What it identifies: Whether local writes are reaching the upload queue, how many are pending, and what operation/data they contain.

Why: ps_crud is the raw upload queue table in the local SQLite database. It is the ground truth for "has this write been recorded by PowerSync?". This is distinct from whether it has been uploaded to the backend. If ps_crud is empty after a write, the write either didn't go through the PowerSync managed table, or transaction.complete() was called prematurely.

How:

SELECT * FROM ps_crud ORDER BY id

What to look for: op (PUT/PATCH/DELETE), type (table name), id, opData (changed columns). If a column you updated is missing from opData, it means its value didn't change from the previous row (PowerSync intentionally omits unchanged values).

Log the Actual endpoint URL in fetchCredentials()

What it identifies: Whether the endpoint value returned by your connector is pointing at the PowerSync Service, not your app backend.

Why: The most common cause of 404 Not Found on /write-checkpoint2.json and /sync/stream is passing the wrong URL as endpoint. PowerSync builds its own request URLs by appending paths to whatever endpoint returns, if that's your app backend, every internal PowerSync request 404s. Adding a log statement catches this immediately.

How: Adding a log statement or set breakpoints to catch the endpoint before fetchCredentials() returns.

Run EXPLAIN QUERY PLAN for Slow Queries

What it identifies: Full table scans, missing indexes, and inefficient joins in client-side SQLite queries.

Why: PowerSync's default JSON-based views extract column values on every row scan, which compounds in joins. Without indexes on join columns, SQLite performs a full scan of every row. EXPLAIN QUERY PLAN makes this visible. A SCAN TABLE without USING INDEX is a red flag.

How:

EXPLAIN QUERY PLAN SELECT ...

What to look for: SCAN TABLE <name> (bad / no index used) vs. SEARCH TABLE <name> USING INDEX (good). If your PowerSync tables show a SCAN, switch to raw tables. If your non-PowerSync tables show a SCAN, add an index on the join column.

Check Package Versions and Duplicate Dependencies

What it identifies: Version mismatches between PowerSync packages and their peers (Drizzle, TanStack, op-sqlite), or duplicate transitive dependencies causing type conflicts.

Why: TypeScript errors with @tanstack/react-db and @powersync/drizzle-driver are often caused by multiple versions of @powersync/common or @tanstack/db installed across direct and transitive dependencies. The packages reference internal types that clash when versions differ.

How:

# Check for multiple PowerSync common versions
npm ls @powersync/common

# Check TanStack version alignment
npm ls @tanstack/react-db @tanstack/powersync-db-collection @tanstack/db

# Check op-sqlite peer dependency
npm ls @powersync/op-sqlite

Also try: Deleting node_modules and the lock file, then reinstalling — stale cached resolutions can cause phantom mismatches.

Verify JWT Claims

What it identifies: Whether your JWT contains the expected sub, aud, iss, exp, kid, and custom claims that PowerSync uses for auth and parameter queries.

Why: JWT issues are the most common connection failure cause. The PowerSync Service validates the kid (key ID) against its configured keystore. A mismatch gives PSYNC_S2101 (See Error Codes Reference). It also enforces exp ≤ 86400s (PSYNC_S2104). Custom claims used in parameter queries (e.g. app_metadata) must be present and structured exactly as the sync rules expect.

How: Paste your JWT into jwt.io or decode it in your debugger.

Check:

  • sub — user ID used in request.user_id()
  • kid — must match a key in PowerSync's keystore (Supabase: legacy vs. JWKS)
  • exp — must be ≤ iat + 86400
  • aud — must match your configured audience
  • Custom claims e.g. app_metadata.my_field must use $.app_metadata.my_field in sync rules

The Sync Diagnostics Client also decodes and displays the active JWT automatically.

Call disconnectAndClear() When Data Looks Wrong After User Switch

What it identifies: Whether stale data from a previous user is polluting the local SQLite database.

Why: disconnect() closes the sync connection but keeps all local data. If you call disconnect() on logout and then connect() with a new user, the new user's UI will initially display the old user's data until sync completes. disconnectAndClear() wipes the local database first, so the new user starts from a clean state.

When to use each:

  • disconnect() — temporary offline, token refresh, app backgrounding. Safe to reconnect as the same user.
  • disconnectAndClear() — user logout, user account switch. Required to prevent data leakage between users.

PSYNC Error Codes

PowerSync has a documented list of error codes with corresponding descriptions.

These error codes are prefixed with PSYNC_, indicating a specific PowerSync related error.

Use them to help drill into specific errors to help debug an issue.

See Error Codes Reference for more information.

Replication Lag Debugging (Postgres)

What it identifies:

  • Sync rules deployment stuck in “processing” for many hours or days (e.g. 2448+ hours)
  • Replication logs show: Replication slot powersync_* is not valid anymore. invalidation_reason: unknown
  • Slot version numbers keep increasing (e.g. 27, 28, 30) as reprocessing restarts
  • Storage usage spikes during reprocessing (expected, but can trigger limit alerts)
  • Source DB is Supabase or another Postgres with default max_slot_wal_keep_size (often 4 GB)

Why:

  • max_slot_wal_keep_size limits how much WAL Postgres keeps for replication slots
  • During initial replication, WAL grows quickly because: (1) full snapshot of all tables in sync rules, (2) ongoing writes on the source DB
  • If replication lag exceeds max_slot_wal_keep_size, Postgres invalidates the slot (wal_status = 'lost')
  • PowerSync detects the invalid slot, creates a new one, and restarts reprocessing
  • With the same limit, the new slot is invalidated again, causing a loop
  • Supabases default 4 GB is often too small for large datasets (e.g. 9+ hour initial replication)

How: Confirm the cause: Check replication slot status and lag.

See Production Readiness Best Practices for the queries and guidance on how to resolve this.