Build real-time, collaborative mobile apps that work seamlessly offline and automatically sync when online. Powered by SQLite Sync.
-
π§© Offline-First, Automatic Sync
Wrap your app withSQLiteSyncProviderto get a local database with automatic, bi-directional cloud synchronization. Your app works fully offline, and all local changes are synced seamlessly when online. -
πͺ React Hooks Designed for Sync-Aware Data
Use hooks likeuseSqliteSyncQueryanduseOnSqliteSyncto automatically refresh your UI when changes are synced from the cloud β keeping your app up-to-date without boilerplate code. -
π§ Zero-Configuration Extension Loading
The SQLite Sync extension is automatically loaded and configured for you. No manual setup required β just access the full SQLite Sync API directly through thedbinstance. -
π± Native-Only, Ultra-Fast
Under the hood, we use OP-SQLite β a low-level, JSI-enabled SQLite engine for React Native With OP-SQLite, database operations run at near-native speed on iOS and Android.
- iOS 13.0+
- Android API 26+
@op-engineering/op-sqlite^15.0.0@react-native-community/netinfo^11.0.0- SQLite Cloud account
β οΈ Note: This library is native-only (iOS/Android).
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/netinfocd ios && pod installNo additional setup required. Native modules are linked automatically.
If using Expo, you must use development builds (Expo Go is not supported):
# Generate native directories
npx expo prebuild
# Run on iOS/Android
npx expo run:ios
# or
npx expo run:android-
Create an account
Sign up at the SQLite Cloud Dashboard. -
Create a database
Follow the database creation guide.Ensure your tables have identical schemas in both local and cloud databases.
-
Enable OffSync
Configure OffSync by following the OffSync setup guide. -
Get credentials
Copy your connection string and API key from the dashboard.- Alternatively, you can use access tokens for Row-Level Security.
The SQLiteSyncProvider needs a createTableSql statement for each table you want to sync. This is required because tables must exist before SQLiteSync initialization.
import { SQLiteSyncProvider } from '@sqliteai/sqlite-sync-react-native';
export default function App() {
return (
<SQLiteSyncProvider
connectionString="sqlitecloud://your-host.sqlite.cloud:8860/your-database"
databaseName="myapp.db"
apiKey="your-api-key"
syncInterval={5000}
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>
);
}The library provides specialized hooks to simplify querying, syncing, and state management.
import { useCallback } from 'react';
import {
useSqliteDb,
useSqliteSyncQuery,
useTriggerSqliteSync,
useOnSqliteSync,
} from '@sqliteai/sqlite-sync-react-native';
function TaskList() {
// 1. DATA FETCHING: Automatically loads local data AND updates on sync
const {
data: tasks,
isLoading,
error,
} = useSqliteSyncQuery<{ id: string; title: string }>(
'SELECT * FROM tasks ORDER BY created_at DESC'
);
// 2. MANUAL SYNC: Control when to sync (e.g., Pull-to-Refresh)
const { triggerSync, isSyncing } = useTriggerSqliteSync();
// 3. EVENT LISTENING: React to incoming changes (e.g., Show a toast)
useOnSqliteSync(() => {
console.log('β¨ New data arrived from the cloud!');
});
// 4. WRITING DATA: Use the optimized hook (no re-renders on sync)
const { db } = useSqliteDb();
const addTask = useCallback(
async (title: string) => {
if (!db) return;
// Use cloudsync_uuid() for conflict-free IDs
await db.execute(
'INSERT INTO tasks (id, title) VALUES (cloudsync_uuid(), ?);',
[title]
);
// Optional: Push changes to cloud immediately
triggerSync();
},
[db, triggerSync]
);
if (isLoading) return <Text>Loading local DB...</Text>;
if (error) return <Text>Error: {error.message}</Text>;
return (
<View>
<Button
title={isSyncing ? 'Syncing...' : 'Sync Now'}
onPress={triggerSync}
disabled={isSyncing}
/>
{tasks.map((task) => (
<Text key={task.id}>{task.title}</Text>
))}
<Button title="Add Random Task" onPress={() => addTask('New Task')} />
</View>
);
}Main provider component that enables sync functionality.
| Prop | Type | Required | Description |
|---|---|---|---|
connectionString |
string |
β | SQLite Cloud connection string |
databaseName |
string |
β | Local database file name |
tablesToBeSynced |
TableConfig[] |
β | Array of tables to sync |
syncInterval |
number |
β | Sync interval in milliseconds |
apiKey |
string |
* | API key for authentication |
accessToken |
string |
* | Access token for RLS authentication |
debug |
boolean |
β | Enable debug logging (default: false) |
children |
ReactNode |
β | Child components |
* Either apiKey or accessToken is required
interface TableConfig {
name: string; // Table name (must match cloud table)
createTableSql: string; // CREATE TABLE SQL statement
}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:
- Always include
IF NOT EXISTSto prevent errors if the table already exists - The table schema must match exactly to your remote table schema in SQLite Cloud
- The library executes this SQL during initialization, before SQLiteSync setup
The library provides three separate React Contexts for optimized re-renders:
Provides database instance and initialization errors. Rarely changes (only on init/error).
import { useContext } from 'react';
import { SQLiteDbContext } from '@sqliteai/sqlite-sync-react-native';
const { db, initError } = useContext(SQLiteDbContext);Values:
| Property | Type | Description |
|---|---|---|
db |
DB | null |
op-sqlite database instance with SQLite Sync extension loaded |
initError |
Error | null |
Fatal database error (db unavailable) |
Provides sync status information. Changes frequently (on every sync).
import { useContext } from 'react';
import { SQLiteSyncStatusContext } from '@sqliteai/sqlite-sync-react-native';
const { isSyncing, lastSyncTime, syncError } = useContext(
SQLiteSyncStatusContext
);Values:
| Property | Type | Description |
|---|---|---|
isSyncReady |
boolean |
Whether sync is configured and ready |
isSyncing |
boolean |
Whether sync is currently in progress |
lastSyncTime |
number | null |
Timestamp of last successful sync |
lastSyncChanges |
number |
Number of changes in last sync |
syncError |
Error | null |
Recoverable sync error (db works offline-only) |
Provides stable sync action functions. Never changes.
import { useContext } from 'react';
import { SQLiteSyncActionsContext } from '@sqliteai/sqlite-sync-react-native';
const { triggerSync } = useContext(SQLiteSyncActionsContext);Values:
| Property | Type | Description |
|---|---|---|
triggerSync |
() => Promise<void> |
Function to manually trigger a sync operation |
subscribe |
(callback: () => void) => () => void |
Subscribe to sync events without re-renders. Returns unsubscribe function |
Note: Most users should use the specialized hooks instead of accessing contexts directly.
About the db instance:
The db property is a DB instance from @op-engineering/op-sqlite with the SQLite Sync extension loaded. This means you can:
- Use the full op-sqlite API for standard database operations
- Use any SQLite Sync functions like
cloudsync_uuid(),cloudsync_changes(), etc.
The library provides specialized hooks for different use cases. Choose the right hook based on what data your component needs to avoid unnecessary re-renders.
Access the database instance and initialization errors without subscribing to sync updates. This hook is optimized for components that only need database access and won't re-render on every sync.
const { db, initError } = useSqliteDb();
if (initError) {
return <Text>Error: {initError.message}</Text>;
}
if (!db) {
return <Text>Loading...</Text>;
}
// Use db for queries without re-rendering on sync
const addTask = async (title: string) => {
await db.execute(
'INSERT INTO tasks (id, title) VALUES (cloudsync_uuid(), ?);',
[title]
);
};Returns:
db: Database instanceinitError: Fatal initialization error
Best for: Components that write data or need database access but don't need sync status.
Access sync status information, use this when you need to display sync state in your UI.
const { isSyncing, lastSyncTime, syncError, isSyncReady, lastSyncChanges } =
useSyncStatus();
return (
<View>
<Text>{isSyncing ? 'Syncing...' : 'Idle'}</Text>
{lastSyncTime && (
<Text>Last sync: {new Date(lastSyncTime).toLocaleTimeString()}</Text>
)}
{syncError && <Text>Sync error: {syncError.message}</Text>}
</View>
);Returns:
isSyncReady: Whether sync is configuredisSyncing: Whether sync is in progresslastSyncTime: Last successful sync timestamplastSyncChanges: Number of changes in last syncsyncError: Recoverable sync error
Best for: Status displays, sync indicators, monitoring components.
Access all sync functionality (database + status + actions). This is a convenience hook that combines all contexts.
Note: This hook will re-render on every sync operation. If you only need db/initError, use useSqliteDb() instead.
const { db, initError, isSyncing, lastSyncTime, triggerSync } = useSqliteSync();
return (
<View>
<Text>Database: {db ? 'Ready' : 'Loading'}</Text>
<Button onPress={triggerSync} disabled={isSyncing} />
</View>
);Returns: All properties from useSqliteDb() + useSyncStatus() + triggerSync function
Best for: Components that need access to everything (use sparingly to avoid unnecessary re-renders).
Manually trigger a sync operation.
How it works: This hook is a convenience wrapper that exposes the triggerSync function from the Provider. The actual sync logic lives in SQLiteSyncProvider to ensure that isSyncing, lastSyncTime, and lastSyncChanges state are updated correctly, allowing all hooks (useOnSqliteSync, useSqliteSyncQuery) to react properly.
const { triggerSync, isSyncing } = useTriggerSqliteSync();
// Trigger sync on button press
<Button
onPress={triggerSync}
disabled={isSyncing}
title={isSyncing ? 'Syncing...' : 'Sync Now'}
/>;Execute a callback when sync completes with changes.
Important:
- This hook does NOT run on initial mount - it's an event listener for sync updates only. For initial data loading, use a separate
useEffectoruseSqliteSyncQuery - This hook uses a subscription pattern that does NOT cause re-renders when sync completes without changes
import { useEffect, useCallback } from 'react';
const loadData = useCallback(async () => {
const result = await db?.execute('SELECT * FROM tasks;');
setTasks(result?.rows || []);
}, [db]);
// 1. Initial Load (Run once when DB is ready)
useEffect(() => {
if (db) loadData();
}, [db, loadData]);
// 2. Sync Updates (Run only when cloud data arrives)
useOnSqliteSync(loadData);Execute a query and automatically re-run when sync updates.
Offline-First: Runs immediately when the database is available, regardless of sync status. This ensures data loads from the local database even when offline.
Auto-Refresh: Re-runs the query automatically when cloud changes arrive via sync. Uses a subscription pattern to avoid unnecessary re-renders - only re-renders when data changes.
Loading States:
isLoading: True only during initial load (when there's no data yet)isRefreshing: True during background updates (sync updates, manual refresh)
const { data, isLoading, isRefreshing, error, refresh } =
useSqliteSyncQuery<Task>('SELECT * FROM tasks ORDER BY created_at DESC');
// Show full-screen spinner only on first load
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return (
<>
{/* Subtle indicator for background updates */}
{isRefreshing && <TopBarSpinner />}
<Button onPress={refresh} title="Refresh" />
<FlatList
data={data}
renderItem={({ item }) => <TaskItem task={item} />}
refreshControl={
<RefreshControl refreshing={isRefreshing} onRefresh={refresh} />
}
/>
</>
);For more fine-grained control over when queries re-execute, you can use op-sqlite's reactive queries directly instead of useSqliteSyncQuery.
The library separates fatal database errors from recoverable sync errors to enable true offline-first operation.
Fatal errors that prevent the database from working at all. The app cannot function when these occur.
const { initError, db } = useSqliteDb();
if (initError) {
return <ErrorScreen message="Database unavailable" />;
}Common causes:
- Unsupported platform (web, Windows, etc.)
- Missing database name
- Failed to open database file
- Failed to create tables
When this happens: The db instance will be null and the app cannot work offline or online.
Recoverable errors that prevent syncing but allow full offline database access.
const { db } = useSqliteDb();
const { syncError } = useSyncStatus();
// Database still works offline even with syncError!
if (db) {
await db.execute('INSERT INTO tasks ...');
}
// Show non-blocking warning
{
syncError && <Banner warning={syncError.message} />;
}Common causes:
- Missing or invalid connection string
- Missing or invalid API key/access token
- SQLiteSync extension failed to load
- Network initialization failed
- Temporary network connectivity issues
When this happens: The db instance is available and fully functional for local-only operations. Sync will be retried on the next interval or when credentials are provided.
Important: Sync errors automatically clear on the next successful sync.
Enable detailed logging during development:
<SQLiteSyncProvider
// ... other props
debug={__DEV__} // Enable in development only
>When enabled, logs:
- Database initialization steps
- Extension loading
- Table creation
- Network setup
- Sync operations
- Change counts
Check out the examples directory for complete working examples:
- sync-demo - Basic sync demonstration with CRUD operations
- SQLite Sync Documentation - Detailed sync docs with API references and best practices
- SQLite Cloud Dashboard - Manage your databases
- OP-SQLite API Reference