Fix: PowerSync Not Working — Offline Sync Failing, Queries Returning Stale Data, or Backend Connection Errors
Quick Answer
How to fix PowerSync issues — SQLite local database, sync rules configuration, backend connector setup, watched queries, offline-first patterns, and React and React Native integration.
The Problem
PowerSync connects but local queries return empty:
const result = await db.getAll('SELECT * FROM todos');
// [] — empty even though the backend database has dataOr data changes locally but doesn’t sync to the server:
await db.execute('INSERT INTO todos (id, title) VALUES (?, ?)', [uuid(), 'New task']);
// Inserts locally but never appears on the serverOr the sync status stays “connecting” indefinitely:
PowerSync: connecting...
PowerSync: connecting...
// Never reaches "connected"Why This Happens
PowerSync provides offline-first sync between a local SQLite database and a remote backend (Postgres, Supabase, MongoDB, etc.):
- Sync rules define what data syncs — PowerSync uses server-defined “sync rules” that control which rows sync to which users. If sync rules don’t match the data or the user’s parameters, no data arrives at the client.
- Writes go through a backend connector — PowerSync’s local SQLite is read-only from the sync perspective. Local writes are queued in an “upload queue” and sent to the server through your custom backend connector. Without a connector implementation, writes stay queued forever.
- The local database schema must match sync rules — the client defines its own SQLite schema that must align with what the sync rules send. Mismatched column names or types cause data to be dropped during sync.
- Authentication must be set up — PowerSync Cloud authenticates clients using JWTs. Without valid credentials, the sync connection fails.
Fix 1: Set Up PowerSync with React
npm install @powersync/react @powersync/web// lib/powersync/schema.ts — local SQLite schema
import { column, Schema, Table } from '@powersync/web';
const todos = new Table({
title: column.text,
completed: column.integer, // SQLite doesn't have boolean — use 0/1
user_id: column.text,
created_at: column.text,
});
const projects = new Table({
name: column.text,
description: column.text,
owner_id: column.text,
});
export const schema = new Schema({ todos, projects });
// TypeScript types (optional but recommended)
export type Todo = {
id: string;
title: string;
completed: number;
user_id: string;
created_at: string;
};
export type Project = {
id: string;
name: string;
description: string;
owner_id: string;
};// lib/powersync/connector.ts — backend connector
import { AbstractPowerSyncDatabase, PowerSyncBackendConnector, UpdateType } from '@powersync/web';
export class BackendConnector implements PowerSyncBackendConnector {
constructor(private apiUrl: string) {}
// Fetch credentials for PowerSync Cloud
async fetchCredentials() {
const res = await fetch(`${this.apiUrl}/api/powersync/token`);
const data = await res.json();
return {
endpoint: data.powersyncUrl, // PowerSync Cloud endpoint
token: data.token, // JWT token
};
}
// Upload local changes to your backend
async uploadData(database: AbstractPowerSyncDatabase) {
const transaction = await database.getNextCrudTransaction();
if (!transaction) return;
try {
for (const op of transaction.crud) {
switch (op.op) {
case UpdateType.PUT:
await fetch(`${this.apiUrl}/api/${op.table}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: op.id, ...op.opData }),
});
break;
case UpdateType.PATCH:
await fetch(`${this.apiUrl}/api/${op.table}/${op.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(op.opData),
});
break;
case UpdateType.DELETE:
await fetch(`${this.apiUrl}/api/${op.table}/${op.id}`, {
method: 'DELETE',
});
break;
}
}
await transaction.complete();
} catch (error) {
console.error('Upload failed:', error);
// Transaction will be retried on next sync
}
}
}// lib/powersync/db.ts — initialize PowerSync
import { PowerSyncDatabase } from '@powersync/web';
import { schema } from './schema';
import { BackendConnector } from './connector';
let powerSyncInstance: PowerSyncDatabase | null = null;
export async function initPowerSync() {
if (powerSyncInstance) return powerSyncInstance;
const db = new PowerSyncDatabase({
schema,
database: { dbFilename: 'myapp.db' },
});
const connector = new BackendConnector(
process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'
);
await db.init();
await db.connect(connector);
powerSyncInstance = db;
return db;
}Fix 2: React Provider and Hooks
// app/providers.tsx
'use client';
import { PowerSyncContext } from '@powersync/react';
import { useEffect, useState } from 'react';
import { initPowerSync } from '@/lib/powersync/db';
import type { PowerSyncDatabase } from '@powersync/web';
export function PowerSyncProvider({ children }: { children: React.ReactNode }) {
const [db, setDb] = useState<PowerSyncDatabase | null>(null);
useEffect(() => {
initPowerSync().then(setDb);
}, []);
if (!db) return <div>Initializing database...</div>;
return (
<PowerSyncContext.Provider value={db}>
{children}
</PowerSyncContext.Provider>
);
}
// app/layout.tsx
import { PowerSyncProvider } from './providers';
export default function RootLayout({ children }) {
return (
<html>
<body>
<PowerSyncProvider>{children}</PowerSyncProvider>
</body>
</html>
);
}// components/TodoList.tsx — reactive queries
'use client';
import { useQuery, useStatus } from '@powersync/react';
import type { Todo } from '@/lib/powersync/schema';
function TodoList({ userId }: { userId: string }) {
// Watched query — re-renders when data changes
const { data: todos, isLoading } = useQuery<Todo>(
'SELECT * FROM todos WHERE user_id = ? ORDER BY created_at DESC',
[userId],
);
const status = useStatus();
if (isLoading) return <div>Loading...</div>;
return (
<div>
{/* Sync status indicator */}
<div>
{status.connected ? '🟢 Online' : '🔴 Offline'}
{status.uploading && ' (uploading...)'}
{status.downloading && ' (downloading...)'}
</div>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed === 1}
onChange={() => toggleTodo(todo.id, todo.completed)}
/>
{todo.title}
</li>
))}
</ul>
</div>
);
}Fix 3: Local Writes (Offline-First)
// hooks/useTodos.ts
'use client';
import { usePowerSync, useQuery } from '@powersync/react';
import type { Todo } from '@/lib/powersync/schema';
import { v4 as uuid } from 'uuid';
export function useTodos(userId: string) {
const db = usePowerSync();
const { data: todos } = useQuery<Todo>(
'SELECT * FROM todos WHERE user_id = ? ORDER BY created_at DESC',
[userId],
);
async function addTodo(title: string) {
// Write to local SQLite — instantly available
await db.execute(
'INSERT INTO todos (id, title, completed, user_id, created_at) VALUES (?, ?, ?, ?, ?)',
[uuid(), title, 0, userId, new Date().toISOString()],
);
// PowerSync queues this write and uploads it via the connector
}
async function toggleTodo(id: string, currentCompleted: number) {
await db.execute(
'UPDATE todos SET completed = ? WHERE id = ?',
[currentCompleted === 1 ? 0 : 1, id],
);
}
async function deleteTodo(id: string) {
await db.execute('DELETE FROM todos WHERE id = ?', [id]);
}
return { todos, addTodo, toggleTodo, deleteTodo };
}
// Usage in component
function TodoApp({ userId }: { userId: string }) {
const { todos, addTodo, toggleTodo, deleteTodo } = useTodos(userId);
return (
<div>
<button onClick={() => addTodo('New task')}>Add Todo</button>
{todos.map(todo => (
<div key={todo.id}>
<input
type="checkbox"
checked={todo.completed === 1}
onChange={() => toggleTodo(todo.id, todo.completed)}
/>
{todo.title}
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</div>
))}
</div>
);
}Fix 4: Sync Rules (Server-Side)
Sync rules define which data syncs to which users:
# sync-rules.yaml — PowerSync Cloud configuration
bucket_definitions:
# User's own data
user_data:
parameters: SELECT token_parameters.user_id AS user_id
data:
- SELECT * FROM todos WHERE user_id = bucket.user_id
- SELECT * FROM projects WHERE owner_id = bucket.user_id
# Shared project data
project_members:
parameters:
SELECT project_id FROM project_members
WHERE user_id = token_parameters.user_id
data:
- SELECT * FROM projects WHERE id = bucket.project_id
- SELECT * FROM todos WHERE project_id = bucket.project_idFix 5: Token Endpoint (Authentication)
// app/api/powersync/token/route.ts
import jwt from 'jsonwebtoken';
import { auth } from '@/auth';
export async function GET() {
const session = await auth();
if (!session?.user) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
// Create a JWT for PowerSync with user-specific parameters
const token = jwt.sign(
{
sub: session.user.id,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 300, // 5 minutes
parameters: {
user_id: session.user.id,
},
},
process.env.POWERSYNC_PRIVATE_KEY!,
{ algorithm: 'RS256' },
);
return Response.json({
token,
powersyncUrl: process.env.NEXT_PUBLIC_POWERSYNC_URL!,
});
}Fix 6: Conflict Resolution
// Backend connector — handle conflicts during upload
async uploadData(database: AbstractPowerSyncDatabase) {
const transaction = await database.getNextCrudTransaction();
if (!transaction) return;
try {
for (const op of transaction.crud) {
if (op.op === UpdateType.PATCH) {
// Send with last known server version for conflict detection
const res = await fetch(`${this.apiUrl}/api/${op.table}/${op.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...op.opData,
_version: op.metadata?.version, // For optimistic concurrency
}),
});
if (res.status === 409) {
// Conflict — server has a newer version
// PowerSync will re-sync the latest server data
console.warn('Conflict detected for', op.table, op.id);
continue; // Skip this op, let sync resolve it
}
}
}
await transaction.complete();
} catch (error) {
// Will retry on next sync cycle
}
}Still Not Working?
Queries return empty after connecting — sync rules might not match the user’s token parameters. Check that token_parameters.user_id in sync rules matches the parameters.user_id in the JWT. Also verify the table names in sync rules match the Postgres tables exactly.
Writes queue but never upload — the uploadData method in your connector is either not implemented, throwing errors silently, or not calling transaction.complete(). Add logging to the upload method. Also check that the backend API endpoints your connector calls are accessible and returning success responses.
Sync status stays “connecting” — the PowerSync Cloud endpoint URL or token is invalid. Check that NEXT_PUBLIC_POWERSYNC_URL is set correctly. Verify the JWT token endpoint returns valid tokens. Enable debug logging to see connection errors.
Data syncs down but local writes are lost on page reload — PowerSync persists the local SQLite database using IndexedDB (web) or the filesystem (React Native). If the browser clears IndexedDB, local data is lost. Ensure writes are uploaded before the user navigates away by checking status.uploading.
For related offline-first and database issues, see Fix: ElectricSQL Not Working and Fix: Turso Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: ElectricSQL Not Working — Sync Not Starting, Shapes Empty, or Postgres Connection Failing
How to fix ElectricSQL issues — Postgres setup with logical replication, shape definitions, real-time sync to the client, React hooks, write-path through the server, and deployment configuration.
Fix: Astro DB Not Working — Tables Not Found, Queries Failing, or Seed Data Missing
How to fix Astro DB issues — schema definition, seed data, queries with drizzle, local development, remote database sync, and Astro Studio integration.
Fix: Expo Router Not Working — Routes Not Matching, Layout Nesting Wrong, or Deep Links Failing
How to fix Expo Router issues — file-based routing, layout routes, dynamic segments, tabs and stack navigation, modal routes, authentication flows, and deep linking configuration.
Fix: React Native Paper Not Working — Theme Not Applying, Icons Missing, or Components Unstyled
How to fix React Native Paper issues — PaperProvider setup, Material Design 3 theming, custom color schemes, icon configuration, dark mode, and Expo integration.