Offline overview
Offline mode in FFDB is for apps that must keep working through flaky or absent connectivity. You provide a local database adapter, optionally a network adapter, and the client handles queued writes plus pull and push synchronization.
Why use offline mode
Choose offline mode when at least one of these is true:
- Name
Your users lose network often- Description
Mobile, desktop, and field apps need read/write continuity while disconnected.
- Name
You want local-first responsiveness- Description
Reads from local storage can feel instant while remote sync runs in the background.
- Name
You need reliable write delivery- Description
Mutation queueing allows writes to flush after connectivity returns.
Interface surfaces
The main contracts you wire are the offline adapter, network adapter, and sync status.
type OfflineAdapter = {
execute(sql: string, params?: unknown[]): Promise<{ rows: Record<string, unknown>[] }> | { rows: Record<string, unknown>[] }
close?(): Promise<void> | void
}
type NetworkAdapter = {
isOnline(): boolean | Promise<boolean>
subscribe(listener: (online: boolean) => void): () => void
}
type OfflineConfig = {
adapter: OfflineAdapter
network?: NetworkAdapter
tables?: string[]
skipTables?: string[]
syncOnConnect?: boolean
syncInterval?: number
syncOnReconnect?: boolean
syncOnFocus?: boolean
maxRowsPerTable?: number
pageSize?: number
}
type SyncStatus = {
isSyncing: boolean
isOnline: boolean
lastSyncedAt: number | null
pendingMutations: number
error: Error | null
}
End-to-end setup flow
This pattern is the baseline for browser or desktop apps with local SQLite support.
import { createClient } from 'ffdb-client'
import type { Database } from './ffdb.types'
const offlineAdapter = {
async execute(sql: string, params: unknown[] = []) {
// Replace with your environment-specific SQLite execution.
const rows = await runSQLiteQuery(sql, params)
return { rows }
},
}
const { db, sync } = await createClient<Database>({
config: {
apiUrl: import.meta.env.VITE_FFDB_API_URL,
},
offline: {
adapter: offlineAdapter,
syncOnConnect: true,
syncOnReconnect: true,
syncOnFocus: true,
syncInterval: 30_000,
maxRowsPerTable: 10_000,
pageSize: 500,
},
})
await db
.insertInto('task')
.values({ id: crypto.randomUUID(), title: 'Field note', completed: 0 })
.execute()
if (sync) {
await sync.run()
console.log(sync.status)
}
How sync flows in practice
- Local writes are recorded while offline.
- When online, sync pushes pending mutations.
- Sync then pulls fresh table data and updates local cache.
- Subscribers can react to data/status changes.
This sequence is why sync helpers expose methods such as run, push, pull, waitForIdle, and status.
Configuration tradeoffs
- Name
tables- Description
Use when only specific tables should be hydrated locally. Good for least-data sync strategies.
- Name
skipTables- Description
Use to exclude large or low-value tables from local sync.
- Name
syncInterval- Description
Improves freshness, but increases battery/network usage in mobile contexts.
- Name
syncOnFocus- Description
Good for browser UX. Avoid if focus churn causes too many background syncs.
- Name
maxRowsPerTable and pageSize- Description
Balance initial sync speed vs memory/network pressure.
Do not treat runtime in-memory cache as durable offline storage. Use proper storage and offline adapters for restart-safe behavior.
Monitoring sync status
if (sync) {
const unsubscribe = sync.subscribe(() => {
const { isSyncing, isOnline, pendingMutations, error } = sync.status
console.log({ isSyncing, isOnline, pendingMutations, error })
})
await sync.waitForIdle(10_000)
unsubscribe()
}