Loading...
Loading...
PowerSync offline-first sync engine on SQLite for React Native - schema definition, watched queries, CRUD operations, backend connectors, sync rules, conflict resolution, attachments
npx skill4agent add agents-inc/skills mobile-storage-sqlite-powersyncQuick Guide: Usefor offline-first apps backed by local SQLite. Define schemas with@powersync/react-nativeandTable(id column is auto-created). Usecolumn.text/integer/realfor reads/writes,PowerSyncDatabasefromuseQueryfor reactive watched queries. Connect to your backend via a connector implementing@powersync/react+fetchCredentials. Conflict resolution defaults to last-write-wins per field -- customize inuploadData. UseuploadDatafor SQLCipher encryption.@powersync/op-sqlite
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,, named constants)import type
new Table({ ... })column.textcolumn.integercolumn.realidpowersync.connect(connector)init()fetchCredentials()uploadData()useQuery@powersync/reactgetAll()TablecolumnPowerSyncDatabaseuseQueryuseSuspenseQueryuseStatususePowerSyncfetchCredentials()uploadData()execute()get()getAll()getOptional()AttachmentTableAttachmentQueueuploadData()useQueryClient (React Native) Cloud
+-------------------+ +-------------------+
| Local SQLite DB | <-sync->| PowerSync Service |<--- Source DB (Postgres, etc.)
| (PowerSyncDatabase)| | (Sync Rules) |
+-------------------+ +-------------------+
| @powersync/react | | Bucket Defs |
| (useQuery, etc.) | | (YAML config) |
+-------------------+ +-------------------+execute(INSERT/UPDATE/DELETE)uploadData()useQuerycolumn.textcolumn.integercolumn.realidTablecolumnidimport { column, Schema, Table } from "@powersync/react-native";
const lists = new Table({
created_at: column.text,
name: column.text,
owner_id: column.text,
});
const todos = new Table(
{
list_id: column.text,
created_at: column.text,
completed_at: column.text,
description: column.text,
completed: column.integer,
},
{ indexes: { list: ["list_id"] } },
);
export const AppSchema = new Schema({ todos, lists });
// Derive types from schema
export type Database = (typeof AppSchema)["types"];
export type TodoRecord = Database["todos"];
export type ListRecord = Database["lists"];idimport { PowerSyncDatabase } from "@powersync/react-native";
import { AppSchema } from "./schema";
const DB_FILENAME = "app.db";
export const powersync = new PowerSyncDatabase({
schema: AppSchema,
database: { dbFilename: DB_FILENAME },
});
// Initialize and connect (typically in app bootstrap)
async function initDatabase(connector: PowerSyncBackendConnector) {
await powersync.init();
await powersync.connect(connector);
}init()connect()connect()useQuery@powersync/reactPowerSyncContext.Providerimport { useQuery, useStatus, usePowerSync } from "@powersync/react";
function TodoList({ listId }: { listId: string }) {
const {
data: todos,
isLoading,
error,
} = useQuery<TodoRecord>(
"SELECT * FROM todos WHERE list_id = ? ORDER BY created_at DESC",
[listId],
);
if (isLoading) return <ActivityIndicator />;
if (error) return <Text>Error: {error.message}</Text>;
return (
<FlatList
data={todos}
renderItem={({ item }) => <TodoItem todo={item} />}
keyExtractor={(item) => item.id}
/>
);
}useQuerytodosisLoadingerroruseSuspenseQueryuseStatususePowerSyncrunQueryOnceexecute()uploadData()import { usePowerSync } from "@powersync/react";
function useTodos(listId: string) {
const powersync = usePowerSync();
const addTodo = async (description: string) => {
await powersync.execute(
"INSERT INTO todos (id, list_id, description, created_at, completed) VALUES (uuid(), ?, ?, datetime(), 0)",
[listId, description],
);
};
const toggleTodo = async (id: string, completed: boolean) => {
const completedAt = completed ? new Date().toISOString() : null;
await powersync.execute(
"UPDATE todos SET completed = ?, completed_at = ? WHERE id = ?",
[completed ? 1 : 0, completedAt, id],
);
};
const deleteTodo = async (id: string) => {
await powersync.execute("DELETE FROM todos WHERE id = ?", [id]);
};
return { addTodo, toggleTodo, deleteTodo };
}uuid()execute(){ rowsAffected, insertId }rowsAffectedRETURNINGget()getAll()getOptional()fetchCredentials()uploadData()import type {
PowerSyncBackendConnector,
PowerSyncCredentials,
} from "@powersync/react-native";
import type { AbstractPowerSyncDatabase } from "@powersync/react-native";
export const connector: PowerSyncBackendConnector = {
fetchCredentials: async (): Promise<PowerSyncCredentials> => {
// Return your PowerSync instance URL and a valid JWT
const session = await getAuthSession();
return {
endpoint: POWERSYNC_URL,
token: session.accessToken,
expiresAt: session.expiresAt,
};
},
uploadData: async (database: AbstractPowerSyncDatabase): Promise<void> => {
const transaction = await database.getNextCrudTransaction();
if (!transaction) return;
for (const op of transaction.crud) {
// Send each operation to your backend API
await applyOperation(op);
}
await transaction.complete();
},
};complete()bucket_definitions:
user_lists:
parameters: SELECT request.user_id() as user_id
data:
- SELECT * FROM lists WHERE owner_id = bucket.user_id
- SELECT * FROM todos WHERE list_id IN (
SELECT id FROM lists WHERE owner_id = bucket.user_id
)
global_settings:
# No parameters = global bucket, synced to all clients
data:
- SELECT * FROM settingsuploadData()uploadData()CrudEntryAttachmentTableAttachmentQueueimport { AttachmentTable } from "@powersync/react-native";
import { column, Schema, Table } from "@powersync/react-native";
const users = new Table({
name: column.text,
photo_id: column.text, // References attachment ID
});
export const AppSchema = new Schema({
users,
attachments: new AttachmentTable(),
});AttachmentQueueWhat kind of data are you storing?
|
+-> Relational data that needs offline + cloud sync?
| +-> YES -> PowerSync + SQLite (this skill)
| +-> NO -> Key-value pairs only?
| +-> YES -> Use a key-value store (not this skill)
| +-> NO -> Files/media only?
| +-> YES -> Use the filesystem
|
+-> Do you need reactive queries in React?
| +-> YES -> Use useQuery from @powersync/react
| +-> NO -> Use powersync.getAll() / get() directly
|
+-> Do you need on-device encryption?
| +-> YES -> Use @powersync/op-sqlite with SQLCipher
| +-> NO -> Use the default SQLite adapter
|
+-> Do you have file attachments?
| +-> YES -> Use AttachmentTable + AttachmentQueue
| +-> NO -> Standard schema is sufficient
|
+-> How should conflicts be resolved?
+-> Simple apps -> Last-write-wins (default)
+-> Collaborative editing -> Field-level merge or CRDTs
+-> Business-critical -> Server-side validation + conflict recording| Scenario | API |
|---|---|
| Reactive component data | |
| Reactive with Suspense | |
| One-time fetch (no reactivity) | |
| Service/utility reads | |
| Write operations | |
| Connection status | |
| Database instance access | |
ididtextexecute()getAll()useQuery()execute()powersync.connect(connector)getAll()useQuery()transaction.complete()uploadData()fetchCredentials()AttachmentTablecolumn.integeruuid()column.realrequest.user_id()getNextCrudTransaction()nullexecute()rowsAffected: 0RETURNINGclientImplementation: SyncClientImplementation.JAVASCRIPTdisconnectAndClear()disconnect()localOnly: trueAll code must follow project conventions in CLAUDE.md
new Table({ ... })column.textcolumn.integercolumn.realidpowersync.connect(connector)init()fetchCredentials()uploadData()useQuery@powersync/reactgetAll()