npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sqliteai/sqlite-sync-react-native

v1.0.6

Published

Offline-first React Native library with automatic sqlite-sync powered by SQLite Cloud

Readme

@sqliteai/sqlite-sync-react-native

Status: Beta npm version License: MIT

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.

Table of Contents

Compatibility

| 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, and expo-application are needed for push token registration.
  • expo-task-manager and expo-secure-store are additionally required for notificationListening="always".
  • Testing push notifications require a real device. Simulators and emulators are not enough for a full push flow.

Choose Your Auth Model

| 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 |

Choose Your Sync Mode

| 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 |

Installation

1. Install Required Dependencies

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/netinfo

Optional Expo packages for push mode:

npx expo install expo-notifications expo-constants expo-application expo-secure-store expo-task-manager

2. Platform Setup

Expo

If 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-properties

Add this plugin to app.json or app.config.js:

["expo-build-properties", { "android": { "minSdkVersion": 26 } }]

Quick Start

1. Set Up SQLite Cloud

  1. Create an account at the SQLite Cloud Dashboard.
  2. Create a database by following the database creation guide.
  3. Create your tables in SQLite Cloud.
  4. Enable OffSync by following the OffSync setup guide.
  5. Copy your databaseId and either your apiKey or plan to provide the current signed-in user's accessToken.

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.

2. Wrap Your App

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>
  );
}

3. Read Data Reactively

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>}
    />
  );
}

4. Write Data With Transactions

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')} />;
}

5. Show Sync Status And Manual Sync

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>
  );
}

Sync Behavior

The library provides lifecycle-aware synchronization that adapts to app state, network availability, and previous sync activity.

Sync Triggers

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

Adaptive Polling Algorithm

In polling mode, the sync interval changes based on activity:

  1. Default state: use baseInterval (default 5000)
  2. Idle backoff: after emptyThreshold consecutive empty syncs (default 5), the interval increases gradually based on idleBackoffMultiplier
  3. Error backoff: on sync failures, the interval increases more aggressively based on errorBackoffMultiplier
  4. Reset on activity: any sync with changes resets the interval to baseInterval
  5. 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 Mode

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-notifications for push notification handling
  • expo-constants for EAS project ID lookup
  • expo-application for device ID during push token registration
  • expo-task-manager for background/terminated notification handling when notificationListening="always"
  • expo-secure-store for persisted background sync config when notificationListening="always"

Setup:

  1. Install the Expo packages listed above:
npx expo install expo-notifications expo-constants expo-application expo-secure-store expo-task-manager
  1. If you use notificationListening="always", configure background notifications in app.json / app.config.js:
{
  "expo": {
    "plugins": [
      ["expo-notifications", { "enableBackgroundRemoteNotifications": true }]
    ],
    "ios": {
      "infoPlist": {
        "UIBackgroundModes": ["remote-notification"]
      }
    }
  }
}
  1. 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.json in your project and connect those credentials to Expo
  1. 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.

notificationListening Modes

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

API Reference

SQLiteSyncProvider

Main provider component that enables sync functionality.

Props

| 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 |

Sync Modes

// 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} />
  )}
>

Background Sync Callback

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,
  });
});

Database Migrations With onDatabaseReady

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');
    }
  }}
>

Custom Push Permission UI With renderPushPermissionPrompt

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>

AdaptivePollingConfig

interface AdaptivePollingConfig {
  baseInterval?: number;
  maxInterval?: number;
  emptyThreshold?: number;
  idleBackoffMultiplier?: number;
  errorBackoffMultiplier?: number;
}

Defaults:

  • baseInterval: 5000
  • maxInterval: 300000
  • emptyThreshold: 5
  • idleBackoffMultiplier: 1.5
  • errorBackoffMultiplier: 2.0

TableConfig

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

ReactiveQueryConfig

interface ReactiveQueryConfig {
  query: string;
  arguments?: any[];
  fireOn: Array<{
    table: string;
    operation?: 'INSERT' | 'UPDATE' | 'DELETE';
  }>;
}

TableUpdateData<T>

interface TableUpdateData<T = any> {
  table: string;
  operation: 'INSERT' | 'UPDATE' | 'DELETE';
  rowId: number;
  row: T | null;
}

Notes:

  • rowId is SQLite's internal rowid, not your application primary key
  • For DELETE, row is null

TableUpdateConfig<T>

interface TableUpdateConfig<T = any> {
  tables: string[];
  onUpdate: (data: TableUpdateData<T>) => void;
}

Contexts

The library exposes three React contexts.

SQLiteDbContext

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 writes
  • readDb: 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 |

SQLiteSyncStatusContext

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 |

SQLiteSyncActionsContext

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.

Hooks

useSqliteDb()

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 connection
  • readDb: read-only database connection
  • initError: fatal initialization error

Use this when:

  • You need direct DB access
  • You do not want re-renders on sync state changes

Note: writeDb and readDb are DB instances from @op-engineering/op-sqlite. The writeDb connection has SQLite Sync loaded, so you can call standard OP-SQLite APIs and SQLite Sync functions such as cloudsync_uuid() or cloudsync_changes().

useSyncStatus()

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

useSqliteSync()

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

useTriggerSqliteSync()

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'}
/>;

useSqliteSyncQuery(config)

Execute a reactive query using OP-SQLite's reactiveExecute.

How it works:

  • Performs the initial fetch with readDb when 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.

useOnTableUpdate(config)

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>): void

Example:

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, row is null
  • rowId is SQLite internal state, not your domain ID

useSqliteExecute()

Execute SQL imperatively with configurable connection selection.

Connection selection:

  • Default: writeDb
  • Pass { readOnly: true } to use readDb
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.

useSqliteTransaction()

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 autoSync is false

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.

Error Handling

The library separates fatal database errors from recoverable sync errors so the app can continue to work offline whenever possible.

Database Errors (initError)

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:

  • writeDb is null
  • readDb is null
  • The app cannot operate offline or online

Sync Errors (syncError)

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 apiKey or accessToken
  • SQLite Sync extension failed to load
  • Network initialization failed
  • Temporary network connectivity issues

When this happens:

  • writeDb and readDb remain available
  • The app still works offline
  • Sync retries later when conditions improve

Sync errors clear automatically after the next successful sync.

Debug Logging

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

Known Issues & Improvements

Reactive Queries And Write Connection Contention

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.

Optimistic Updates And Sync Blocking

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.

First Install Empty State

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.

Examples

See the examples directory:

Links