Offline-first React Native sync for SQLite Cloud. This library gives you a local SQLite database on-device, keeps it usable offline, and synchronizes changes with SQLite Cloud when connectivity is available.
Powered by SQLite Sync and OP-SQLite.
- Compatibility
- Choose Your Auth Model
- Choose Your Sync Mode
- Installation
- Quick Start
- Sync Behavior
- API Reference
- Error Handling
- Debug Logging
- Known Issues & Improvements
- Examples
- Links
| Requirement | Status / Minimum |
|---|---|
| React Native | Native projects and Expo development builds |
| iOS | 13.0+ |
| Android | API 26+ |
| Web | Not supported |
| Expo Go | Not supported |
| SQLite engine | @op-engineering/op-sqlite ^15.1.14 |
| Network status | @react-native-community/netinfo ^11.0.0 |
| Cloud backend | SQLite Cloud |
Optional Expo dependencies for push mode:
Notes:
expo-notifications,expo-constants, andexpo-applicationare needed for push token registration.expo-task-managerandexpo-secure-storeare additionally required fornotificationListening="always".- Testing push notifications require a real device. Simulators and emulators are not enough for a full push flow.
| Auth prop | Use when | Notes |
|---|---|---|
apiKey |
Your app uses database-level access | Simpler setup |
accessToken |
Your app uses SQLite Cloud access tokens / RLS | Use this for signed-in user auth |
| Mode | Best for | Requirements | Tradeoffs |
|---|---|---|---|
polling |
Most apps, easiest setup, predictable behavior | No Expo push packages required | Checks periodically instead of instantly |
push |
Apps that need near real-time sync triggers | Expo push setup and permissions | More setup, may fall back to polling |
npm install @sqliteai/sqlite-sync-react-native @op-engineering/op-sqlite @react-native-community/netinfo
# or
yarn add @sqliteai/sqlite-sync-react-native @op-engineering/op-sqlite @react-native-community/netinfoOptional Expo packages for push mode:
npx expo install expo-notifications expo-constants expo-application expo-secure-store expo-task-managerIf you use Expo, you must use development builds. Expo Go is not supported because this library depends on native modules.
Set Android minSdkVersion to 26:
npx expo install expo-build-propertiesAdd this plugin to app.json or app.config.js:
["expo-build-properties", { "android": { "minSdkVersion": 26 } }]- Create an account at the SQLite Cloud Dashboard.
- Create a database by following the database creation guide.
- Create your tables in SQLite Cloud.
- Enable OffSync by following the OffSync setup guide.
- Copy your
databaseIdand either yourapiKeyor plan to provide the current signed-in user'saccessToken.
Schema requirements matter:
- Your local table schema must exactly match the cloud table schema.
- For SQLite Sync schema best practices, see SQLite Sync Best Practices.
The provider needs a createTableSql statement for every table you want synchronized. That SQL is executed locally before sync initialization.
import { SQLiteSyncProvider } from '@sqliteai/sqlite-sync-react-native';
export default function App() {
return (
<SQLiteSyncProvider
databaseId="db_xxxxxxxxxxxxxxxxxxxxxxxx"
databaseName="myapp.db"
apiKey="your-api-key"
syncMode="polling"
tablesToBeSynced={[
{
name: 'tasks',
createTableSql: `
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY NOT NULL,
title TEXT,
completed INTEGER DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
`,
},
]}
>
<YourApp />
</SQLiteSyncProvider>
);
}Use useSqliteSyncQuery for UI reads that should automatically update when data changes locally or from sync.
import { useSqliteSyncQuery } from '@sqliteai/sqlite-sync-react-native';
interface Task {
id: string;
title: string;
completed: number;
}
function TaskList() {
const {
data: tasks,
isLoading,
error,
} = useSqliteSyncQuery<Task>({
query: 'SELECT * FROM tasks ORDER BY created_at DESC',
arguments: [],
fireOn: [{ table: 'tasks' }],
});
if (isLoading) return <Text>Loading...</Text>;
if (error) return <Text>Error: {error.message}</Text>;
return (
<FlatList
data={tasks}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <Text>{item.title}</Text>}
/>
);
}Use useSqliteTransaction for app writes that should trigger reactive queries.
import { useSqliteTransaction } from '@sqliteai/sqlite-sync-react-native';
function AddTaskButton() {
const { executeTransaction } = useSqliteTransaction();
const addTask = async (title: string) => {
await executeTransaction(async (tx) => {
await tx.execute(
'INSERT INTO tasks (id, title) VALUES (cloudsync_uuid(), ?);',
[title]
);
});
};
return <Button title="Add Task" onPress={() => addTask('New Task')} />;
}Use useTriggerSqliteSync for manual sync and useSyncStatus to render status.
import {
useTriggerSqliteSync,
useSyncStatus,
} from '@sqliteai/sqlite-sync-react-native';
function SyncControls() {
const { triggerSync } = useTriggerSqliteSync();
const { isSyncing, lastSyncTime, syncError, syncMode } = useSyncStatus();
return (
<View>
<Text>Mode: {syncMode}</Text>
<Button
title={isSyncing ? 'Syncing...' : 'Sync Now'}
onPress={triggerSync}
disabled={isSyncing}
/>
{lastSyncTime && (
<Text>Last sync: {new Date(lastSyncTime).toLocaleTimeString()}</Text>
)}
{syncError && <Text>Sync error: {syncError.message}</Text>}
</View>
);
}The library provides lifecycle-aware synchronization that adapts to app state, network availability, and previous sync activity.
Primary triggers:
- App start -> immediate sync
- App resume from background -> immediate sync, debounced to 5 seconds
- Network reconnection -> immediate sync
Secondary triggers:
- Polling mode: Periodic polling while app is foregrounded
- Push mode: Push notification from SQLite Cloud triggers sync when server changes are available
In polling mode, the sync interval changes based on activity:
- Default state: use
baseInterval(default5000) - Idle backoff: after
emptyThresholdconsecutive empty syncs (default5), the interval increases gradually based onidleBackoffMultiplier - Error backoff: on sync failures, the interval increases more aggressively based on
errorBackoffMultiplier - Reset on activity: any sync with changes resets the interval to
baseInterval - Foreground priority: resuming the app triggers an immediate sync and resets the interval
Example idle progression:
5s -> 7.5s -> 11.25s -> ...
Example error progression:
5s -> 10s -> 20s -> 40s -> ...
Example timeline:
App Start: Sync (0 changes) -> Next in 5s
5s later: Sync (0 changes) -> Next in 5s
10s later: Sync (0 changes) -> Next in 5s
15s later: Sync (0 changes) -> Next in 5s
20s later: Sync (0 changes) -> Next in 5s
25s later: Sync (0 changes) -> Next in 7.5s
32.5s later: Sync (0 changes) -> Next in 11.25s
43.75s later: Sync (5 changes) -> Next in 5s
App backgrounded: Polling paused
App foregrounded: Sync immediately -> Next in 5s
Push notifications from SQLite Cloud trigger sync when there are changes to fetch. Sync still happens on app start, foreground, and network reconnect for reliability.
Requirements:
expo-notificationsfor push notification handlingexpo-constantsfor EAS project ID lookupexpo-applicationfor device ID during push token registrationexpo-task-managerfor background/terminated notification handling whennotificationListening="always"expo-secure-storefor persisted background sync config whennotificationListening="always"
Setup:
- Install the Expo packages listed above:
npx expo install expo-notifications expo-constants expo-application expo-secure-store expo-task-manager- If you use
notificationListening="always", configure background notifications inapp.json/app.config.js:
{
"expo": {
"plugins": [
["expo-notifications", { "enableBackgroundRemoteNotifications": true }]
],
"ios": {
"infoPlist": {
"UIBackgroundModes": ["remote-notification"]
}
}
}
}- Configure push credentials by following the Expo Push Notifications setup guide.
- iOS: use a paid Apple Developer account, register the physical iOS device you want to test on, and let EAS set up push notifications and generate an APNs key for the app during your first development build
- Android: create a Firebase project, add an Android app in Firebase with the same package name as your Expo app, set up FCM V1 credentials, then place the generated
google-services.jsonin your project and connect those credentials to Expo
- If you use Expo enhanced security, configure your Expo access token in SQLite Cloud Dashboard > OffSync > Configuration. The Expo access token is optional unless enhanced security is enabled.
| App State | notificationListening="foreground" |
notificationListening="always" |
|---|---|---|
| Foreground | Notification triggers sync on the existing DB connection | Same behavior |
| Background | Notification ignored | Background task opens a DB connection, syncs, then calls registerBackgroundSyncCallback if registered |
| Terminated | Notification ignored | Background task wakes the app, opens a DB connection, syncs, then calls registerBackgroundSyncCallback if registered |
Note: If push mode cannot be used because required Expo packages are missing, notification permissions are denied, or push token retrieval fails, the provider logs a warning and falls back to polling mode.
Main provider component that enables sync functionality.
| Prop | Type | Required | Description |
|---|---|---|---|
databaseId |
string |
Yes | SQLite Sync database ID used by runtime sync APIs |
databaseName |
string |
Yes | Local database file name |
tablesToBeSynced |
TableConfig[] |
Yes | Array of tables to sync |
apiKey |
string |
Conditionally | API key authentication |
accessToken |
string |
Conditionally | Signed-in user access token authentication |
syncMode |
'polling' | 'push' |
Yes | Sync mode |
adaptivePolling |
AdaptivePollingConfig |
No | Polling configuration; defaults are used when omitted |
notificationListening |
'foreground' | 'always' |
No | Push listening behavior |
renderPushPermissionPrompt |
(props: { allow: () => void; deny: () => void }) => ReactNode |
No | Custom pre-permission UI for push mode |
onDatabaseReady |
(db: DB) => Promise<void> |
No | Called after DB opens and before sync init |
debug |
boolean |
No | Enable debug logs |
children |
ReactNode |
Yes | App content |
// Polling mode with runtime defaults
<SQLiteSyncProvider
databaseId="db_xxxxxxxxxxxxxxxxxxxxxxxx"
databaseName="myapp.db"
apiKey="your-api-key"
tablesToBeSynced={[...]}
syncMode="polling"
>
// Polling mode with custom intervals
<SQLiteSyncProvider
databaseId="db_xxxxxxxxxxxxxxxxxxxxxxxx"
databaseName="myapp.db"
apiKey="your-api-key"
tablesToBeSynced={[...]}
syncMode="polling"
adaptivePolling={{
baseInterval: 3000,
maxInterval: 60000,
emptyThreshold: 3
}}
>
// Push mode - foreground only
<SQLiteSyncProvider
databaseId="db_xxxxxxxxxxxxxxxxxxxxxxxx"
databaseName="myapp.db"
apiKey="your-api-key"
tablesToBeSynced={[...]}
syncMode="push"
notificationListening="foreground"
>
// Push mode - foreground + background + terminated
<SQLiteSyncProvider
databaseId="db_xxxxxxxxxxxxxxxxxxxxxxxx"
databaseName="myapp.db"
apiKey="your-api-key"
tablesToBeSynced={[...]}
syncMode="push"
notificationListening="always"
renderPushPermissionPrompt={({ allow, deny }) => (
<YourCustomPermissionDialog onAllow={allow} onDeny={deny} />
)}
>When using push mode with notificationListening="always", you can register a callback that runs after a background sync completes.
import { registerBackgroundSyncCallback } from '@sqliteai/sqlite-sync-react-native';
import * as Notifications from 'expo-notifications';
registerBackgroundSyncCallback(async ({ changes, db }) => {
const newItems = changes.filter(
(c) => c.table === 'tasks' && c.operation === 'INSERT'
);
if (newItems.length === 0) return;
const result = await db.execute(
`SELECT * FROM tasks WHERE rowid IN (${newItems
.map((c) => c.rowId)
.join(',')})`
);
await Notifications.scheduleNotificationAsync({
content: {
title: `${newItems.length} new tasks synced`,
body: result.rows?.[0]?.title || 'New data available',
},
trigger: null,
});
});Use onDatabaseReady to run migrations or setup after the database opens and before sync initialization.
<SQLiteSyncProvider
databaseId="db_xxxxxxxxxxxxxxxxxxxxxxxx"
databaseName="myapp.db"
apiKey="..."
tablesToBeSynced={[...]}
onDatabaseReady={async (db) => {
const { rows } = await db.execute('PRAGMA user_version');
const version = rows?.[0]?.user_version ?? 0;
if (version < 1) {
await db.execute('ALTER TABLE tasks ADD COLUMN priority INTEGER DEFAULT 0');
await db.execute('PRAGMA user_version = 1');
}
}}
>Use renderPushPermissionPrompt to show your own UI before the system notification permission prompt appears.
<SQLiteSyncProvider
databaseId="db_xxxxxxxxxxxxxxxxxxxxxxxx"
databaseName="myapp.db"
apiKey="your-api-key"
tablesToBeSynced={[...]}
syncMode="push"
renderPushPermissionPrompt={({ allow, deny }) => (
<Modal visible animationType="fade" transparent>
<View style={{ flex: 1, justifyContent: 'center', padding: 24 }}>
<Text>Enable notifications for real-time sync?</Text>
<TouchableOpacity onPress={allow}>
<Text>Enable</Text>
</TouchableOpacity>
<TouchableOpacity onPress={deny}>
<Text>Not Now</Text>
</TouchableOpacity>
</View>
</Modal>
)}
>
<YourApp />
</SQLiteSyncProvider>interface AdaptivePollingConfig {
baseInterval?: number;
maxInterval?: number;
emptyThreshold?: number;
idleBackoffMultiplier?: number;
errorBackoffMultiplier?: number;
}Defaults:
baseInterval:5000maxInterval:300000emptyThreshold:5idleBackoffMultiplier:1.5errorBackoffMultiplier:2.0
interface TableConfig {
name: string;
createTableSql: string;
}Example:
{
name: 'users',
createTableSql: `
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY NOT NULL,
name TEXT,
email TEXT UNIQUE,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
`
}Important:
- Include
IF NOT EXISTS - Match the remote schema exactly
- The library executes this SQL during initialization before SQLite Sync setup
interface ReactiveQueryConfig {
query: string;
arguments?: any[];
fireOn: Array<{
table: string;
operation?: 'INSERT' | 'UPDATE' | 'DELETE';
}>;
}interface TableUpdateData<T = any> {
table: string;
operation: 'INSERT' | 'UPDATE' | 'DELETE';
rowId: number;
row: T | null;
}Notes:
rowIdis SQLite's internalrowid, not your application primary key- For
DELETE,rowisnull
interface TableUpdateConfig<T = any> {
tables: string[];
onUpdate: (data: TableUpdateData<T>) => void;
}The library exposes three React contexts.
Provides database connections and fatal initialization errors. This context changes rarely.
Dual-connection architecture:
writeDb: write connection used for sync operations, reactive subscriptions, update hooks, and writesreadDb: read-only connection for read-only queries
Both connections target the same database file and use WAL mode for concurrent access.
import { useContext } from 'react';
import { SQLiteDbContext } from '@sqliteai/sqlite-sync-react-native';
const { writeDb, readDb, initError } = useContext(SQLiteDbContext);| Property | Type | Description |
|---|---|---|
writeDb |
DB | null |
Write op-sqlite connection with SQLite Sync loaded |
readDb |
DB | null |
Read-only op-sqlite connection |
initError |
Error | null |
Fatal database initialization error |
Provides sync status information. This context changes frequently.
import { useContext } from 'react';
import { SQLiteSyncStatusContext } from '@sqliteai/sqlite-sync-react-native';
const { isSyncing, lastSyncTime, syncError, syncMode, currentSyncInterval } =
useContext(SQLiteSyncStatusContext);| Property | Type | Description |
|---|---|---|
syncMode |
'polling' | 'push' |
Effective runtime sync mode |
isSyncReady |
boolean |
Whether sync is configured and ready |
isSyncing |
boolean |
Whether sync is in progress |
lastSyncTime |
number | null |
Timestamp of last successful sync |
lastSyncChanges |
number |
Number of changes in last sync |
syncError |
Error | null |
Recoverable sync error |
currentSyncInterval |
number | null |
Current polling interval, or null in push mode |
consecutiveEmptySyncs |
number |
Consecutive syncs with no changes |
consecutiveSyncErrors |
number |
Consecutive sync errors |
isAppInBackground |
boolean |
Whether the app is currently backgrounded |
isNetworkAvailable |
boolean |
Whether network connectivity is available |
Provides stable sync action functions.
import { useContext } from 'react';
import { SQLiteSyncActionsContext } from '@sqliteai/sqlite-sync-react-native';
const { triggerSync } = useContext(SQLiteSyncActionsContext);| Property | Type | Description |
|---|---|---|
triggerSync |
() => Promise<void> |
Manually trigger a sync |
Most applications should prefer the specialized hooks instead of consuming contexts directly.
Access the database connections and initialization errors without subscribing to sync status updates.
useSqliteDb(): {
writeDb: DB | null;
readDb: DB | null;
initError: Error | null;
}const { writeDb, readDb, initError } = useSqliteDb();Returns:
writeDb: write database connectionreadDb: read-only database connectioninitError: fatal initialization error
Use this when:
- You need direct DB access
- You do not want re-renders on sync state changes
Note:
writeDbandreadDbareDBinstances from@op-engineering/op-sqlite. ThewriteDbconnection has SQLite Sync loaded, so you can call standard OP-SQLite APIs and SQLite Sync functions such ascloudsync_uuid()orcloudsync_changes().
Access sync status information for UI state.
useSyncStatus(): {
syncMode: 'polling' | 'push';
isSyncReady: boolean;
isSyncing: boolean;
lastSyncTime: number | null;
lastSyncChanges: number;
syncError: Error | null;
currentSyncInterval: number | null;
consecutiveEmptySyncs: number;
consecutiveSyncErrors: number;
isAppInBackground: boolean;
isNetworkAvailable: boolean;
}const { syncMode, isSyncing, lastSyncTime, syncError, currentSyncInterval } =
useSyncStatus();
return (
<View>
<Text>Mode: {syncMode}</Text>
<Text>{isSyncing ? 'Syncing...' : 'Idle'}</Text>
{lastSyncTime && (
<Text>Last sync: {new Date(lastSyncTime).toLocaleTimeString()}</Text>
)}
{currentSyncInterval && (
<Text>Next sync: {currentSyncInterval / 1000}s</Text>
)}
{syncError && <Text>Sync error: {syncError.message}</Text>}
</View>
);Use this when:
- You need to render sync state
- You want to inspect the effective runtime
syncMode
Convenience hook that combines DB access, sync status, and actions.
useSqliteSync(): {
writeDb: DB | null;
readDb: DB | null;
initError: Error | null;
syncMode: 'polling' | 'push';
isSyncReady: boolean;
isSyncing: boolean;
lastSyncTime: number | null;
lastSyncChanges: number;
syncError: Error | null;
currentSyncInterval: number | null;
consecutiveEmptySyncs: number;
consecutiveSyncErrors: number;
isAppInBackground: boolean;
isNetworkAvailable: boolean;
triggerSync: () => Promise<void>;
}const { writeDb, initError, isSyncing, lastSyncTime, triggerSync } =
useSqliteSync();
return (
<View>
<Text>Database: {writeDb ? 'Ready' : 'Loading'}</Text>
<Button onPress={triggerSync} disabled={isSyncing} />
</View>
);Use this when:
- You want one hook for everything
- You accept more frequent re-renders
Manually trigger a sync operation.
useTriggerSqliteSync(): {
triggerSync: () => Promise<void>;
}const { triggerSync } = useTriggerSqliteSync();
const { isSyncing } = useSyncStatus();
<Button
onPress={triggerSync}
disabled={isSyncing}
title={isSyncing ? 'Syncing...' : 'Sync Now'}
/>;Execute a reactive query using OP-SQLite's reactiveExecute.
How it works:
- Performs the initial fetch with
readDbwhen available - Installs the reactive subscription on
writeDb - Re-runs the query when watched tables change
interface ReactiveQueryConfig {
query: string;
arguments?: any[];
fireOn: Array<{
table: string;
operation?: 'INSERT' | 'UPDATE' | 'DELETE';
}>;
}useSqliteSyncQuery<T = any>(config: ReactiveQueryConfig): {
data: T[];
isLoading: boolean;
error: Error | null;
unsubscribe: () => void;
}Example:
const { data, isLoading, error } = useSqliteSyncQuery<Task>({
query: 'SELECT * FROM tasks WHERE user_id = ? ORDER BY created_at DESC',
arguments: [userId],
fireOn: [{ table: 'tasks' }, { table: 'task_assignments' }],
});
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return (
<FlatList data={data} renderItem={({ item }) => <TaskItem task={item} />} />
);Important:
- Reactive queries fire on committed transactions
- Use
useSqliteTransaction()for writes that should invalidate reactive queries
const { execute } = useSqliteExecute();
const { executeTransaction } = useSqliteTransaction();
// Triggers reactive queries
await executeTransaction(async (tx) => {
await tx.execute('INSERT INTO tasks (id, title) VALUES (?, ?)', [id, title]);
});
// Does not trigger reactive queries
await execute('INSERT INTO tasks (id, title) VALUES (?, ?)', [id, title]);Sync operations are already wrapped in transactions internally, so cloud-driven changes will update reactive queries.
Listen for row-level INSERT, UPDATE, and DELETE events using OP-SQLite's updateHook.
interface TableUpdateConfig<T = any> {
tables: string[];
onUpdate: (data: TableUpdateData<T>) => void;
}
interface TableUpdateData<T = any> {
table: string;
operation: 'INSERT' | 'UPDATE' | 'DELETE';
rowId: number;
row: T | null;
}useOnTableUpdate<T = any>(config: TableUpdateConfig<T>): voidExample:
interface Task {
id: string;
title: string;
completed: boolean;
}
useOnTableUpdate<Task>({
tables: ['tasks', 'notes'],
onUpdate: (data) => {
console.log(`Table: ${data.table}`);
console.log(`Operation: ${data.operation}`);
if (data.row) {
Toast.show(
`Task "${data.row.title}" was ${data.operation.toLowerCase()}d`
);
analytics.track('task_modified', {
operation: data.operation,
taskId: data.row.id,
});
} else {
console.log('Row was deleted');
}
},
});Note:
- For
DELETE,rowisnull rowIdis SQLite internal state, not your domain ID
Execute SQL imperatively with configurable connection selection.
Connection selection:
- Default:
writeDb - Pass
{ readOnly: true }to usereadDb
interface SqliteExecuteOptions {
readOnly?: boolean;
autoSync?: boolean;
}useSqliteExecute(): {
execute: (
sql: string,
params?: any[],
options?: SqliteExecuteOptions
) => Promise<QueryResult | undefined>;
isExecuting: boolean;
error: Error | null;
}Example:
import { useSqliteExecute } from '@sqliteai/sqlite-sync-react-native';
function TaskManager() {
const { execute, isExecuting, error } = useSqliteExecute();
const addTask = async (title: string) => {
try {
const result = await execute(
'INSERT INTO tasks (id, title) VALUES (cloudsync_uuid(), ?)',
[title]
);
console.log('Inserted row ID:', result?.insertId);
} catch (err) {
console.error('Failed to insert:', err);
}
};
const getTask = async (id: string) => {
try {
const result = await execute('SELECT * FROM tasks WHERE id = ?', [id], {
readOnly: true,
});
return result?.rows?.[0];
} catch (err) {
console.error('Failed to fetch:', err);
}
};
return (
<View>
<Button
title="Add Task"
onPress={() => addTask('New Task')}
disabled={isExecuting}
/>
{error && <Text>Error: {error.message}</Text>}
</View>
);
}Important:
- Direct
execute()writes do not trigger reactive queries - Use
useSqliteTransaction()when you need reactive invalidation
This hook automatically sends local write changes to the cloud unless you disable it:
await execute(
'INSERT INTO local_cache (key, value) VALUES (?, ?)',
[key, value],
{ autoSync: false }
);If you call db.execute() directly through OP-SQLite, automatic cloud send does not happen for you.
Execute SQL commands within a transaction for atomic writes.
useSqliteTransaction(): {
executeTransaction: (
fn: (tx: Transaction) => Promise<void>,
options?: { autoSync?: boolean }
) => Promise<void>;
isExecuting: boolean;
error: Error | null;
}Example:
import { useSqliteTransaction } from '@sqliteai/sqlite-sync-react-native';
function TaskManager() {
const { executeTransaction, isExecuting } = useSqliteTransaction();
const addTaskWithLog = async (title: string) => {
try {
await executeTransaction(async (tx) => {
await tx.execute(
'INSERT INTO tasks (id, title) VALUES (cloudsync_uuid(), ?)',
[title]
);
await tx.execute('INSERT INTO logs (action, timestamp) VALUES (?, ?)', [
'task_created',
Date.now(),
]);
});
console.log('Task and log inserted successfully');
} catch (err) {
console.error('Transaction failed:', err);
}
};
const addLocalOnlyTask = async (title: string) => {
await executeTransaction(
async (tx) => {
await tx.execute('INSERT INTO tasks (id, title) VALUES (?, ?)', [
'local-id',
title,
]);
},
{ autoSync: false }
);
};
return (
<Button
title="Add Task"
onPress={() => addTaskWithLog('New Task')}
disabled={isExecuting}
/>
);
}Important:
- Transactions trigger reactive queries on successful commit
- This is the recommended write path for data displayed with
useSqliteSyncQuery - The hook auto-sends changes to the cloud after commit unless
autoSyncisfalse
To skip automatic sync:
await executeTransaction(
async (tx) => {
await tx.execute('INSERT INTO local_cache (key, value) VALUES (?, ?)', [
key,
value,
]);
},
{ autoSync: false }
);Note: If you use
db.transaction()directly through OP-SQLite, automatic cloud send does not happen for you.
The library separates fatal database errors from recoverable sync errors so the app can continue to work offline whenever possible.
These are fatal. The database is unavailable.
const { initError, writeDb } = useSqliteDb();
if (initError) {
return <ErrorScreen message="Database unavailable" />;
}Common causes:
- Unsupported platform
- Missing database name
- Failed to open the database file
- Failed to create tables
When this happens:
writeDbisnullreadDbisnull- The app cannot operate offline or online
These are recoverable. The local database still works.
const { writeDb } = useSqliteDb();
const { syncError } = useSyncStatus();
if (writeDb) {
await writeDb.execute('INSERT INTO tasks ...');
}
{
syncError && <Banner warning={syncError.message} />;
}Common causes:
- Invalid
databaseId - Invalid
apiKeyoraccessToken - SQLite Sync extension failed to load
- Network initialization failed
- Temporary network connectivity issues
When this happens:
writeDbandreadDbremain available- The app still works offline
- Sync retries later when conditions improve
Sync errors clear automatically after the next successful sync.
Enable verbose development logging with the debug prop:
<SQLiteSyncProvider
databaseId="db_xxxxxxxxxxxxxxxxxxxxxxxx"
databaseName="myapp.db"
apiKey="your-api-key"
tablesToBeSynced={[...]}
syncMode="polling"
debug={__DEV__}
>When enabled, logs include:
- Database initialization steps
- Extension loading
- Table creation
- Network setup
- Sync operations
- Change counts
Issue:
useSqliteSyncQuery uses writeDb with OP-SQLite's reactiveExecute so queries see sync changes immediately. Under heavy write or sync activity, the write connection can become a bottleneck.
Potential improvement:
Have cloudsync_network_sync() return updated table names so reactive queries could read from readDb and invalidate manually.
Issue:
In push-heavy scenarios, sync can occupy writeDb frequently enough that local writes may feel delayed.
Potential improvement:
Introduce optimistic UI updates independent of sync completion, with conflict-resolution rules when sync finishes.
Issue:
On first install, the initial sync may not immediately return data, so the app can briefly render an empty state before a later sync populates it.
Potential improvement:
Keep showing a loader until the first successful sync with data, or until a timeout expires.
See the examples directory:
examples/sync-demo-expo: Expo development build using push notificationsexamples/sync-demo-bare: Bare React Native example using polling