local-lite 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +421 -362
  2. package/dist/adapters/local/better-sqlite3.d.ts +2 -0
  3. package/dist/adapters/local/better-sqlite3.js +46 -25
  4. package/dist/adapters/local/expo-sqlite.d.ts +5 -1
  5. package/dist/adapters/local/expo-sqlite.js +40 -7
  6. package/dist/adapters/local/index.d.ts +1 -0
  7. package/dist/adapters/local/node-sqlite.d.ts +2 -0
  8. package/dist/adapters/local/node-sqlite.js +45 -22
  9. package/dist/adapters/local/op-sqlite.d.ts +1 -0
  10. package/dist/adapters/local/op-sqlite.js +6 -0
  11. package/dist/adapters/local/sql-js.d.ts +1 -0
  12. package/dist/adapters/local/sql-js.js +5 -0
  13. package/dist/adapters/network/index.js +33 -0
  14. package/dist/adapters/remote/convex.d.ts +2 -2
  15. package/dist/adapters/remote/convex.js +8 -12
  16. package/dist/adapters/remote/demo.d.ts +1 -1
  17. package/dist/adapters/remote/demo.js +3 -0
  18. package/dist/adapters/remote/firestore.d.ts +3 -2
  19. package/dist/adapters/remote/firestore.js +69 -46
  20. package/dist/adapters/remote/index.d.ts +6 -4
  21. package/dist/adapters/remote/supabase.d.ts +34 -6
  22. package/dist/adapters/remote/supabase.js +340 -79
  23. package/dist/config/index.js +7 -10
  24. package/dist/config/resolve-local.js +12 -1
  25. package/dist/config/resolve-remote.js +33 -28
  26. package/dist/config/types.d.ts +30 -5
  27. package/dist/core/client.d.ts +86 -3
  28. package/dist/core/client.js +423 -41
  29. package/dist/core/index.d.ts +14 -0
  30. package/dist/core/index.js +29 -0
  31. package/dist/core/outbox-store.d.ts +8 -4
  32. package/dist/core/outbox-store.js +84 -65
  33. package/dist/core/query.d.ts +8 -0
  34. package/dist/core/query.js +37 -0
  35. package/dist/core/schema-manager.d.ts +9 -0
  36. package/dist/core/schema-manager.js +68 -0
  37. package/dist/core/sync-worker.d.ts +38 -1
  38. package/dist/core/sync-worker.js +351 -104
  39. package/dist/core/table-manager.js +26 -29
  40. package/dist/core/types.d.ts +8 -0
  41. package/dist/react/BenchmarkModal.d.ts +32 -0
  42. package/dist/react/BenchmarkModal.js +777 -0
  43. package/dist/react/SyncInspector.d.ts +31 -0
  44. package/dist/react/SyncInspector.js +655 -0
  45. package/dist/react/index.d.ts +51 -19
  46. package/dist/react/index.js +265 -105
  47. package/dist/react/runner.d.ts +7 -0
  48. package/dist/react/runner.js +42 -0
  49. package/dist/react/status.d.ts +24 -0
  50. package/dist/react/status.js +123 -0
  51. package/dist/react/theme.d.ts +18 -0
  52. package/dist/react/theme.js +46 -0
  53. package/dist/utils/logger.d.ts +4 -0
  54. package/dist/utils/logger.js +20 -0
  55. package/package.json +138 -133
package/README.md CHANGED
@@ -1,362 +1,421 @@
1
- # ⚡ local-lite
2
-
3
- A lightweight, vendor-agnostic **local-first synchronization engine** for React Native, Expo, and Web applications.
4
-
5
- Built around the **Transactional Outbox Pattern**, `local-lite` ensures instant, zero-latency local writes in SQLite while guaranteeing offline resilience and asynchronous synchronization with your backend (including native **Supabase**, **Convex**, and **Firebase / Firestore** support).
6
-
7
- ---
8
-
9
- ## 📖 Documentation
10
-
11
- * **[Quickstart & Setup](docs/getting-started.md)** - 5-minute setup with Expo, React Native, or Node.js
12
- * **[Architecture](docs/architecture.md)** - Transactional outbox pattern, SyncWorker loops, and event pipeline
13
- * **[Configuration Reference](docs/configuration.md)** - Options for `defineSyncConfig()`, retries, and loggers
14
- * **[Local SQLite Adapters](docs/adapters-local.md)** - Expo SQLite, OP-SQLite, better-sqlite3, node:sqlite, and LibSQL
15
- * **[Remote Cloud Adapters](docs/adapters-remote.md)** - Supabase, Convex, Cloud Firestore, and custom adapters
16
- * **[React & Expo Hooks](docs/react-hooks.md)** - `useLiveQuery()` and `useSyncStatus()`
17
- * **[Conflict Resolution](docs/conflict-resolution.md)** - LWW, Client-Wins, Server-Wins, and custom resolvers
18
-
19
- ---
20
-
21
- ## Features
22
-
23
- * **⚡ 0ms Optimistic Writes:** Write to local SQLite immediately via atomic dual-write (`client.mutate()`).
24
- * **📦 Transactional Outbox:** Mutations are stored atomically in a local `sync_outbox` table, preventing data loss if the app closes or loses connectivity mid-write.
25
- * **🌐 Native Cloud Adapters:** First-class sync with Postgres tables (via **Supabase**), reactive document datastores (via **Convex**), and document collections (via **Firebase / Firestore**).
26
- * **🛡️ Conflict Resolution:** Built-in strategies including **Last-Write-Wins (LWW)**, Server-Wins, Client-Wins, or custom resolver functions.
27
- * **📶 Network-Aware:** Automatically pauses polling when offline and triggers immediate sync flushes the instant connectivity is restored.
28
- * **🔄 Infinite-Loop Safe:** Incoming remote deltas update local tables and fire reactive UI listeners without echoing back into the outbox.
29
-
30
- ---
31
-
32
- ## Installation
33
-
34
- ```bash
35
- npm install local-lite
36
- ```
37
-
38
- Peer dependencies (install what you use):
39
-
40
- ```bash
41
- # If using Expo SQLite
42
- npx expo install expo-sqlite @react-native-community/netinfo
43
-
44
- # If syncing with Supabase
45
- npm install @supabase/supabase-js
46
-
47
- # If syncing with Convex
48
- npm install convex
49
-
50
- # If syncing with Firebase / Firestore
51
- npm install firebase # or @react-native-firebase/firestore
52
- ```
53
-
54
- ---
55
-
56
- ### 1. Define your Configuration (`sync.config.ts`)
57
-
58
- #### Option A: With Supabase
59
-
60
- ```typescript
61
- // sync.config.ts
62
- import { defineSyncConfig, createSyncClient } from 'local-lite';
63
- import * as SQLite from 'expo-sqlite';
64
-
65
- export const syncConfig = defineSyncConfig({
66
- database: () => SQLite.openDatabaseAsync('app.db'),
67
- tables: ['todos', 'notes'],
68
- autoSchema: true,
69
-
70
- // Supabase backend configuration
71
- supabase: {
72
- url: process.env.EXPO_PUBLIC_SUPABASE_URL || 'https://xyz.supabase.co',
73
- anonKey: process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY || 'YOUR_ANON_KEY',
74
- },
75
-
76
- conflictResolver: 'last-write-wins',
77
- });
78
-
79
- export const client = await createSyncClient(syncConfig);
80
- ```
81
-
82
- #### Option B: With Convex
83
-
84
- ```typescript
85
- // sync.config.ts
86
- import { defineSyncConfig, createSyncClient } from 'local-lite';
87
- import { ConvexClient } from 'convex/browser';
88
- import * as SQLite from 'expo-sqlite';
89
-
90
- const convexClient = new ConvexClient(process.env.EXPO_PUBLIC_CONVEX_URL!);
91
-
92
- export const syncConfig = defineSyncConfig({
93
- database: () => SQLite.openDatabaseAsync('app.db'),
94
- tables: ['properties', 'tenants'],
95
- autoSchema: true,
96
-
97
- // Convex backend configuration
98
- convex: {
99
- client: convexClient,
100
- pushMutation: 'sync:pushBatch', // Or api.sync.pushBatch
101
- pullQuery: 'sync:pullDeltas', // Or api.sync.pullDeltas
102
- deletedColumn: 'deleted',
103
- updatedAtColumn: 'updatedAt',
104
- },
105
-
106
- conflictResolver: 'last-write-wins',
107
- });
108
-
109
- export const client = await createSyncClient(syncConfig);
110
- ```
111
-
112
- #### Option C: With Firebase / Cloud Firestore
113
-
114
- ```typescript
115
- // sync.config.ts
116
- import { defineSyncConfig, createSyncClient } from 'local-lite';
117
- import { initializeApp } from 'firebase/app';
118
- import { getFirestore } from 'firebase/firestore';
119
- import * as SQLite from 'expo-sqlite';
120
-
121
- const app = initializeApp({
122
- apiKey: process.env.EXPO_PUBLIC_FIREBASE_API_KEY,
123
- projectId: process.env.EXPO_PUBLIC_FIREBASE_PROJECT_ID,
124
- });
125
- const db = getFirestore(app);
126
-
127
- export const syncConfig = defineSyncConfig({
128
- database: () => SQLite.openDatabaseAsync('app.db'),
129
- tables: ['properties', 'tenants'],
130
- autoSchema: true,
131
-
132
- // Firebase Firestore backend configuration
133
- firebase: {
134
- firestore: db, // Modular Firestore or @react-native-firebase/firestore
135
- updatedAtColumn: 'updatedAt', // Timestamp field used for delta sync
136
- deletedColumn: 'deleted', // Soft delete flag
137
- },
138
-
139
- conflictResolver: 'last-write-wins',
140
- });
141
-
142
- export const client = await createSyncClient(syncConfig);
143
- ```
144
-
145
- ---
146
-
147
- ### 2. Use in React Native / Expo UI
148
-
149
- ```tsx
150
- import { useLiveQuery, useSyncStatus } from 'local-lite/react';
151
- import { client } from './sync.config';
152
-
153
- export function TodoScreen() {
154
- // ⚡ Zero-SQL Reactive Live Query (Auto re-renders on local writes AND background remote pulls!)
155
- const { data: todos } = useLiveQuery(client, 'todos', {
156
- orderBy: 'id',
157
- order: 'desc',
158
- });
159
-
160
- const { pendingCount, isOnline, isSyncing } = useSyncStatus(client);
161
-
162
- const addTodo = async (title: string) => {
163
- await client.insert('todos', {
164
- title,
165
- completed: 0,
166
- });
167
- };
168
-
169
- const toggleTodo = async (id: string, completed: number) => {
170
- await client.update('todos', id, { completed: completed ? 0 : 1 });
171
- };
172
-
173
- const removeTodo = async (id: string) => {
174
- await client.delete('todos', id);
175
- };
176
-
177
- return (
178
- <View>
179
- <Text>Pending outbox items: {pendingCount}</Text>
180
- {todos.map(t => <Text key={t.id}>{t.title}</Text>)}
181
- </View>
182
- );
183
- }
184
- ```
185
-
186
- ---
187
-
188
- ## Supabase Backend Setup & Schema
189
-
190
- To synchronize with Supabase using zero-clock-skew delta queries and `deleted: true` soft deletes, execute the following script in your Supabase project (**Dashboard → SQL Editor**):
191
-
192
- ```sql
193
- -- 1. Automatic Timestamp Trigger
194
- CREATE OR REPLACE FUNCTION public.set_sync_updated_at()
195
- RETURNS TRIGGER AS $$
196
- BEGIN
197
- NEW.updated_at = timezone('utc'::text, now());
198
- RETURN NEW;
199
- END;
200
- $$ LANGUAGE plpgsql;
201
-
202
- -- 2. Create Synced Table (e.g. 'todos')
203
- CREATE TABLE IF NOT EXISTS public.todos (
204
- id TEXT PRIMARY KEY,
205
- title TEXT NOT NULL,
206
- completed INTEGER DEFAULT 0,
207
- deleted BOOLEAN DEFAULT FALSE NOT NULL,
208
- created_at TIMESTAMPTZ DEFAULT timezone('utc'::text, now()) NOT NULL,
209
- updated_at TIMESTAMPTZ DEFAULT timezone('utc'::text, now()) NOT NULL
210
- );
211
-
212
- -- 3. Index for high-performance delta sync queries
213
- CREATE INDEX IF NOT EXISTS idx_todos_sync ON public.todos (updated_at);
214
-
215
- -- 4. Auto-update updated_at on changes (including soft deletes)
216
- CREATE OR REPLACE TRIGGER trigger_todos_updated_at
217
- BEFORE UPDATE ON public.todos
218
- FOR EACH ROW EXECUTE FUNCTION public.set_sync_updated_at();
219
-
220
- -- 5. Enable Supabase Realtime broadcast replication
221
- ALTER PUBLICATION supabase_realtime ADD TABLE public.todos;
222
- ```
223
-
224
- ---
225
-
226
- ## Convex Backend Setup & Functions
227
-
228
- In Convex, create a sync module (e.g., `convex/sync.ts`):
229
-
230
- ```typescript
231
- // convex/sync.ts
232
- import { mutation, query } from "./_generated/server";
233
- import { v } from "convex/values";
234
-
235
- // 1. Push batch mutation from outbox
236
- export const pushBatch = mutation({
237
- args: {
238
- operations: v.array(
239
- v.object({
240
- id: v.string(),
241
- tableName: v.string(),
242
- rowId: v.string(),
243
- operation: v.string(),
244
- payload: v.any(),
245
- timestamp: v.number(),
246
- })
247
- ),
248
- },
249
- handler: async (ctx, args) => {
250
- const now = Date.now();
251
- for (const op of args.operations) {
252
- const table = op.tableName;
253
- const existing = await ctx.db
254
- .query(table as any)
255
- .filter((q) => q.eq(q.field("id"), op.rowId))
256
- .first();
257
-
258
- if (op.operation === "DELETE") {
259
- if (existing) {
260
- await ctx.db.patch(existing._id, { deleted: true, updatedAt: now });
261
- }
262
- } else {
263
- const doc = { ...(op.payload || {}), id: op.rowId, deleted: false, updatedAt: now };
264
- if (existing) {
265
- await ctx.db.patch(existing._id, doc);
266
- } else {
267
- await ctx.db.insert(table as any, doc);
268
- }
269
- }
270
- }
271
- return true;
272
- },
273
- });
274
-
275
- // 2. Pull deltas query since timestamp cursor
276
- export const pullDeltas = query({
277
- args: {
278
- sinceTimestamp: v.number(),
279
- tables: v.array(v.string()),
280
- },
281
- handler: async (ctx, args) => {
282
- const results: any[] = [];
283
- for (const table of args.tables) {
284
- const docs = await ctx.db
285
- .query(table as any)
286
- .filter((q) => q.gt(q.field("updatedAt"), args.sinceTimestamp))
287
- .collect();
288
-
289
- for (const d of docs) {
290
- results.push({ ...d, tableName: table });
291
- }
292
- }
293
- return results;
294
- },
295
- });
296
- ```
297
-
298
- ---
299
-
300
- ## Supported SQLite Drivers
301
-
302
- `local-lite` is fully driver-agnostic and auto-detects your SQLite instance directly:
303
-
304
- | Platform | Recommended Driver | Auto-Detected or Subpath Import |
305
- | :--- | :--- | :--- |
306
- | **Expo (iOS/Android/Web)** | `expo-sqlite` | `database: () => SQLite.openDatabaseAsync('app.db')` |
307
- | **Bare React Native** | `@op-engineering/op-sqlite` | `local-lite/op-sqlite` |
308
- | **Node.js / Electron / Desktop** | `better-sqlite3` | `local-lite/better-sqlite3` |
309
- | **Edge / Server / Turso** | `@libsql/client` (LibSQL) | `local-lite/libsql` |
310
- | **Web Browser / PWAs** | `sql.js` (WebAssembly) | `local-lite/sql-js` |
311
- | **Node.js 22+ (Built-in)** | `node:sqlite` (DatabaseSync) | `local-lite/node-sqlite` |
312
-
313
- ---
314
-
315
- ## Architecture
316
-
317
- ```
318
- [ App UI ]
319
- │
320
- ▼
321
- LocalFirstClient ──(Atomic Transaction)──► Local SQLite Table
322
- │ │
323
- ▼ ▼
324
- sync_outbox (Pending queue) EventEmitter (UI refresh)
325
- │
326
- ▼ (Immediate / Debounced / Network-Aware)
327
- SyncWorker
328
- │
329
- ├──► pushChanges() ──► Remote Backend (Supabase / Convex / Firebase / Custom)
330
- │
331
- └──◄ fetchChanges() ◄── Remote Deltas (Merged via Conflict Resolver)
332
- ```
333
-
334
- ---
335
-
336
- ## Example App
337
-
338
- An interactive Expo app is included in the [`example/`](./example) directory demonstrating:
339
- - Rental property & tenant management application
340
- - Live SQLite reads and atomic dual-writes
341
- - Network online/offline toggle simulation
342
- - Real-time `sync_outbox` inspector
343
- - Simulated server delta broadcasts
344
-
345
- To run the example:
346
- ```bash
347
- cd example
348
- npm start
349
- ```
350
-
351
- ---
352
-
353
- ## Testing
354
-
355
- Run the Vitest test suite:
356
- ```bash
357
- npm test
358
- ```
359
-
360
- ## License
361
-
362
- MIT
1
+ # ⚡ local-lite
2
+
3
+ [![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-0284c7.svg?style=flat-square)](https://georgechitechi.github.io/local-lite/)
4
+ [![npm version](https://img.shields.io/npm/v/local-lite.svg?style=flat-square)](https://www.npmjs.com/package/local-lite)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
6
+
7
+ A lightweight, vendor-agnostic **local-first synchronization engine** for React Native, Expo, and Web applications.
8
+
9
+ Built around the **Transactional Outbox Pattern**, `local-lite` ensures instant, zero-latency local writes in SQLite while guaranteeing offline resilience and asynchronous synchronization with your backend (including native **Supabase**, **Convex**, and **Firebase / Firestore** support).
10
+
11
+ ---
12
+
13
+ ## 📖 Documentation
14
+
15
+ > 🌐 **Interactive Documentation Site:** **[https://georgechitechi.github.io/local-lite/](https://georgechitechi.github.io/local-lite/)**
16
+
17
+ * **[Quickstart & Setup](https://georgechitechi.github.io/local-lite/#/getting-started)** - 5-minute setup with Expo, React Native, or Node.js
18
+ * **[Architecture](https://georgechitechi.github.io/local-lite/#/architecture)** - Transactional outbox pattern, SyncWorker loops, and event pipeline
19
+ * **[Configuration Reference](https://georgechitechi.github.io/local-lite/#/configuration)** - Options for `defineSyncConfig()`, retries, and loggers
20
+ * **[Local SQLite Adapters](https://georgechitechi.github.io/local-lite/#/adapters-local)** - Expo SQLite, OP-SQLite, better-sqlite3, node:sqlite, and LibSQL
21
+ * **[Remote Cloud Adapters](https://georgechitechi.github.io/local-lite/#/adapters-remote)** - Supabase, Convex, Cloud Firestore, and custom adapters
22
+ * **[React & Expo Hooks](https://georgechitechi.github.io/local-lite/#/react-hooks)** - `useLiveQuery()` and `useSyncStatus()`
23
+ * **[Conflict Resolution](https://georgechitechi.github.io/local-lite/#/conflict-resolution)** - LWW, Client-Wins, Server-Wins, and custom resolvers
24
+
25
+ ---
26
+
27
+ ## Independent core and explicit policies
28
+
29
+ Use `local-lite/core` for the sync engine with your own `LocalDBAdapter` and
30
+ `RemoteDBAdapter`. This entry point does not load provider implementations,
31
+ configuration auto-detection, or React. UI hooks and backend adapters remain optional.
32
+
33
+ ```typescript
34
+ import { LocalFirstClient } from 'local-lite/core';
35
+
36
+ const client = new LocalFirstClient({
37
+ local: myLocalAdapter,
38
+ remote: myRemoteAdapter,
39
+ autoSchema: false,
40
+ autoStart: false,
41
+ conflictResolver: myConflictResolver,
42
+ });
43
+ await client.initialize();
44
+ // Writes remain in the outbox until you push manually or enable automatic sync.
45
+ await client.syncWorker.pushLocalChanges();
46
+ // Or enable polling, reconnect handling, and realtime subscriptions:
47
+ client.syncWorker.start();
48
+ ```
49
+
50
+ Authentication, account switching, application schemas, UI reloads, and decisions
51
+ to discard failed writes belong to the caller. The core does not inspect backend
52
+ error codes to delete failed mutations. Conflict resolution and retry settings
53
+ remain configurable; the existing default conflict strategy is last-write-wins.
54
+
55
+ ### Compatibility changes from 0.0.3
56
+
57
+ - Missing remote configuration now throws. Temporary demo behavior requires
58
+ `inMemoryRemote: true`; it must never silently acknowledge production writes.
59
+ - `autoSchema` now defaults to `false` in the configuration factory as well as the
60
+ core constructor. Set `autoSchema: true` to retain inferred schema behavior.
61
+ - Migrations run transactionally before sync starts, including when initialization
62
+ is deferred with `autoInitialize: false`. Use idempotent SQL; this is not a
63
+ versioned migration framework.
64
+ - `initialize()` is safe to call repeatedly or concurrently. After `stop()`, call
65
+ `client.syncWorker.start()` to resume. Mutations after stop remain queued.
66
+ - `autoStart: false` prepares storage without automatic sync. Explicit push/pull
67
+ methods remain available. Stop does not cancel adapter calls already in flight.
68
+ - Realtime changes update local rows without advancing the durable pull cursor.
69
+ Custom pull adapters must return complete batches through their checkpoint and
70
+ reject partial failures. Supabase table failures now reject the pull.
71
+ - Custom realtime adapters may return an unsubscribe function. Legacy adapters
72
+ returning void remain supported; stopped workers ignore their later callbacks.
73
+
74
+ ## Features
75
+
76
+ Interrupted pushes recover automatically at startup and before the next push cycle,
77
+ including when a temporary database error prevented saving a failure status.
78
+ Use one active sync client per local database. Recovery may replay a remote write,
79
+ so custom remote adapters should handle repeated operation IDs idempotently.
80
+ This recovers sync state; it does not repair corrupt SQLite files or cancel hung adapter calls.
81
+
82
+ * **⚡ 0ms Optimistic Writes:** Write to local SQLite immediately via atomic dual-write (`client.mutate()`).
83
+ * **📦 Transactional Outbox:** Mutations are stored atomically in a local `sync_outbox` table, preventing data loss if the app closes or loses connectivity mid-write.
84
+ * **🌐 Native Cloud Adapters:** First-class sync with Postgres tables (via **Supabase**), reactive document datastores (via **Convex**), and document collections (via **Firebase / Firestore**).
85
+ * **🛡️ Conflict Resolution:** Built-in strategies including **Last-Write-Wins (LWW)**, Server-Wins, Client-Wins, or custom resolver functions.
86
+ * **📶 Network-Aware:** Automatically pauses polling when offline and triggers immediate sync flushes the instant connectivity is restored.
87
+ * **🔄 Infinite-Loop Safe:** Incoming remote deltas update local tables and fire reactive UI listeners without echoing back into the outbox.
88
+
89
+ ---
90
+
91
+ ## Installation
92
+
93
+ ```bash
94
+ npm install local-lite
95
+ ```
96
+
97
+ Peer dependencies (install what you use):
98
+
99
+ ```bash
100
+ # If using Expo SQLite
101
+ npx expo install expo-sqlite @react-native-community/netinfo
102
+
103
+ # If syncing with Supabase
104
+ npm install @supabase/supabase-js
105
+
106
+ # If syncing with Convex
107
+ npm install convex
108
+
109
+ # If syncing with Firebase / Firestore
110
+ npm install firebase # or @react-native-firebase/firestore
111
+ ```
112
+
113
+ ---
114
+
115
+ ### 1. Define your Configuration (`sync.config.ts`)
116
+
117
+ #### Option A: With Supabase
118
+
119
+ ```typescript
120
+ // sync.config.ts
121
+ import { defineSyncConfig, createSyncClient } from 'local-lite';
122
+ import * as SQLite from 'expo-sqlite';
123
+
124
+ export const syncConfig = defineSyncConfig({
125
+ database: () => SQLite.openDatabaseAsync('app.db'),
126
+ tables: ['todos', 'notes'],
127
+ autoSchema: true,
128
+
129
+ // Supabase backend configuration
130
+ supabase: {
131
+ url: process.env.EXPO_PUBLIC_SUPABASE_URL || 'https://xyz.supabase.co',
132
+ anonKey: process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY || 'YOUR_ANON_KEY',
133
+ },
134
+
135
+ conflictResolver: 'last-write-wins',
136
+ });
137
+
138
+ export const client = await createSyncClient(syncConfig);
139
+ ```
140
+
141
+ #### Option B: With Convex
142
+
143
+ ```typescript
144
+ // sync.config.ts
145
+ import { defineSyncConfig, createSyncClient } from 'local-lite';
146
+ import { ConvexClient } from 'convex/browser';
147
+ import * as SQLite from 'expo-sqlite';
148
+
149
+ const convexClient = new ConvexClient(process.env.EXPO_PUBLIC_CONVEX_URL!);
150
+
151
+ export const syncConfig = defineSyncConfig({
152
+ database: () => SQLite.openDatabaseAsync('app.db'),
153
+ tables: ['properties', 'tenants'],
154
+ autoSchema: true,
155
+
156
+ // Convex backend configuration
157
+ convex: {
158
+ client: convexClient,
159
+ pushMutation: 'sync:pushBatch', // Or api.sync.pushBatch
160
+ pullQuery: 'sync:pullDeltas', // Or api.sync.pullDeltas
161
+ deletedColumn: 'deleted',
162
+ updatedAtColumn: 'updatedAt',
163
+ },
164
+
165
+ conflictResolver: 'last-write-wins',
166
+ });
167
+
168
+ export const client = await createSyncClient(syncConfig);
169
+ ```
170
+
171
+ #### Option C: With Firebase / Cloud Firestore
172
+
173
+ ```typescript
174
+ // sync.config.ts
175
+ import { defineSyncConfig, createSyncClient } from 'local-lite';
176
+ import { initializeApp } from 'firebase/app';
177
+ import { getFirestore } from 'firebase/firestore';
178
+ import * as SQLite from 'expo-sqlite';
179
+
180
+ const app = initializeApp({
181
+ apiKey: process.env.EXPO_PUBLIC_FIREBASE_API_KEY,
182
+ projectId: process.env.EXPO_PUBLIC_FIREBASE_PROJECT_ID,
183
+ });
184
+ const db = getFirestore(app);
185
+
186
+ export const syncConfig = defineSyncConfig({
187
+ database: () => SQLite.openDatabaseAsync('app.db'),
188
+ tables: ['properties', 'tenants'],
189
+ autoSchema: true,
190
+
191
+ // Firebase Firestore backend configuration
192
+ firebase: {
193
+ firestore: db, // Modular Firestore or @react-native-firebase/firestore
194
+ updatedAtColumn: 'updatedAt', // Timestamp field used for delta sync
195
+ deletedColumn: 'deleted', // Soft delete flag
196
+ },
197
+
198
+ conflictResolver: 'last-write-wins',
199
+ });
200
+
201
+ export const client = await createSyncClient(syncConfig);
202
+ ```
203
+
204
+ ---
205
+
206
+ ### 2. Use in React Native / Expo UI
207
+
208
+ ```tsx
209
+ import { useLiveQuery, useSyncStatus } from 'local-lite/react';
210
+ import { client } from './sync.config';
211
+
212
+ export function TodoScreen() {
213
+ // ⚡ Zero-SQL Reactive Live Query (Auto re-renders on local writes AND background remote pulls!)
214
+ const { data: todos } = useLiveQuery(client, 'todos', {
215
+ orderBy: 'id',
216
+ order: 'desc',
217
+ });
218
+
219
+ const { pendingCount, isOnline, isSyncing } = useSyncStatus(client);
220
+
221
+ const addTodo = async (title: string) => {
222
+ await client.insert('todos', {
223
+ title,
224
+ completed: 0,
225
+ });
226
+ };
227
+
228
+ const toggleTodo = async (id: string, completed: number) => {
229
+ await client.update('todos', id, { completed: completed ? 0 : 1 });
230
+ };
231
+
232
+ const removeTodo = async (id: string) => {
233
+ await client.delete('todos', id);
234
+ };
235
+
236
+ return (
237
+ <View>
238
+ <Text>Pending outbox items: {pendingCount}</Text>
239
+ {todos.map(t => <Text key={t.id}>{t.title}</Text>)}
240
+ </View>
241
+ );
242
+ }
243
+ ```
244
+
245
+ ---
246
+
247
+ ## Supabase Backend Setup & Schema
248
+
249
+ To synchronize with Supabase using zero-clock-skew delta queries and `deleted: true` soft deletes, execute the following script in your Supabase project (**Dashboard → SQL Editor**):
250
+
251
+ ```sql
252
+ -- 1. Automatic Timestamp Trigger
253
+ CREATE OR REPLACE FUNCTION public.set_sync_updated_at()
254
+ RETURNS TRIGGER AS $$
255
+ BEGIN
256
+ NEW.updated_at = timezone('utc'::text, now());
257
+ RETURN NEW;
258
+ END;
259
+ $$ LANGUAGE plpgsql;
260
+
261
+ -- 2. Create Synced Table (e.g. 'todos')
262
+ CREATE TABLE IF NOT EXISTS public.todos (
263
+ id TEXT PRIMARY KEY,
264
+ title TEXT NOT NULL,
265
+ completed INTEGER DEFAULT 0,
266
+ deleted BOOLEAN DEFAULT FALSE NOT NULL,
267
+ created_at TIMESTAMPTZ DEFAULT timezone('utc'::text, now()) NOT NULL,
268
+ updated_at TIMESTAMPTZ DEFAULT timezone('utc'::text, now()) NOT NULL
269
+ );
270
+
271
+ -- 3. Index for high-performance delta sync queries
272
+ CREATE INDEX IF NOT EXISTS idx_todos_sync ON public.todos (updated_at);
273
+
274
+ -- 4. Auto-update updated_at on changes (including soft deletes)
275
+ CREATE OR REPLACE TRIGGER trigger_todos_updated_at
276
+ BEFORE UPDATE ON public.todos
277
+ FOR EACH ROW EXECUTE FUNCTION public.set_sync_updated_at();
278
+
279
+ -- 5. Enable Supabase Realtime broadcast replication
280
+ ALTER PUBLICATION supabase_realtime ADD TABLE public.todos;
281
+ ```
282
+
283
+ ---
284
+
285
+ ## Convex Backend Setup & Functions
286
+
287
+ In Convex, create a sync module (e.g., `convex/sync.ts`):
288
+
289
+ ```typescript
290
+ // convex/sync.ts
291
+ import { mutation, query } from "./_generated/server";
292
+ import { v } from "convex/values";
293
+
294
+ // 1. Push batch mutation from outbox
295
+ export const pushBatch = mutation({
296
+ args: {
297
+ operations: v.array(
298
+ v.object({
299
+ id: v.string(),
300
+ tableName: v.string(),
301
+ rowId: v.string(),
302
+ operation: v.string(),
303
+ payload: v.any(),
304
+ timestamp: v.number(),
305
+ })
306
+ ),
307
+ },
308
+ handler: async (ctx, args) => {
309
+ const now = Date.now();
310
+ for (const op of args.operations) {
311
+ const table = op.tableName;
312
+ const existing = await ctx.db
313
+ .query(table as any)
314
+ .filter((q) => q.eq(q.field("id"), op.rowId))
315
+ .first();
316
+
317
+ if (op.operation === "DELETE") {
318
+ if (existing) {
319
+ await ctx.db.patch(existing._id, { deleted: true, updatedAt: now });
320
+ }
321
+ } else {
322
+ const doc = { ...(op.payload || {}), id: op.rowId, deleted: false, updatedAt: now };
323
+ if (existing) {
324
+ await ctx.db.patch(existing._id, doc);
325
+ } else {
326
+ await ctx.db.insert(table as any, doc);
327
+ }
328
+ }
329
+ }
330
+ return true;
331
+ },
332
+ });
333
+
334
+ // 2. Pull deltas query since timestamp cursor
335
+ export const pullDeltas = query({
336
+ args: {
337
+ sinceTimestamp: v.number(),
338
+ tables: v.array(v.string()),
339
+ },
340
+ handler: async (ctx, args) => {
341
+ const results: any[] = [];
342
+ for (const table of args.tables) {
343
+ const docs = await ctx.db
344
+ .query(table as any)
345
+ .filter((q) => q.gt(q.field("updatedAt"), args.sinceTimestamp))
346
+ .collect();
347
+
348
+ for (const d of docs) {
349
+ results.push({ ...d, tableName: table });
350
+ }
351
+ }
352
+ return results;
353
+ },
354
+ });
355
+ ```
356
+
357
+ ---
358
+
359
+ ## Supported SQLite Drivers
360
+
361
+ `local-lite` is fully driver-agnostic and auto-detects your SQLite instance directly:
362
+
363
+ | Platform | Recommended Driver | Auto-Detected or Subpath Import |
364
+ | :--- | :--- | :--- |
365
+ | **Expo (iOS/Android/Web)** | `expo-sqlite` | `database: () => SQLite.openDatabaseAsync('app.db')` |
366
+ | **Bare React Native** | `@op-engineering/op-sqlite` | `local-lite/op-sqlite` |
367
+ | **Node.js / Electron / Desktop** | `better-sqlite3` | `local-lite/better-sqlite3` |
368
+ | **Edge / Server / Turso** | `@libsql/client` (LibSQL) | `local-lite/libsql` |
369
+ | **Web Browser / PWAs** | `sql.js` (WebAssembly) | `local-lite/sql-js` |
370
+ | **Node.js 22+ (Built-in)** | `node:sqlite` (DatabaseSync) | `local-lite/node-sqlite` |
371
+
372
+ ---
373
+
374
+ ## Architecture
375
+
376
+ ```
377
+ [ App UI ]
378
+ │
379
+ ▼
380
+ LocalFirstClient ──(Atomic Transaction)──► Local SQLite Table
381
+ │ │
382
+ ▼ ▼
383
+ sync_outbox (Pending queue) EventEmitter (UI refresh)
384
+ │
385
+ ▼ (Immediate / Debounced / Network-Aware)
386
+ SyncWorker
387
+ │
388
+ ├──► pushChanges() ──► Remote Backend (Supabase / Convex / Firebase / Custom)
389
+ │
390
+ └──◄ fetchChanges() ◄── Remote Deltas (Merged via Conflict Resolver)
391
+ ```
392
+
393
+ ---
394
+
395
+ ## Example App
396
+
397
+ An interactive Expo app is included in the [`example/`](./example) directory demonstrating:
398
+ - Rental property & tenant management application
399
+ - Live SQLite reads and atomic dual-writes
400
+ - Network online/offline toggle simulation
401
+ - Real-time `sync_outbox` inspector
402
+ - Simulated server delta broadcasts
403
+
404
+ To run the example:
405
+ ```bash
406
+ cd example
407
+ npm start
408
+ ```
409
+
410
+ ---
411
+
412
+ ## Testing
413
+
414
+ Run the Vitest test suite:
415
+ ```bash
416
+ npm test
417
+ ```
418
+
419
+ ## License
420
+
421
+ MIT