Skip to main content

Introduction

Salve DB is an offline-first SQLite database for React Native. You declare your tables and their REST sync contract as plain TypeScript data; a native C++/Swift/Kotlin core creates the tables, migrates them, installs SQLite triggers that queue every local write, and pushes/pulls against your own REST API — including OAuth2 token refresh — without the JS engine ever being started. A background job (WorkManager on Android, BGTaskScheduler on iOS) wakes the native sync orchestrator on its own; your app doesn't need to be open.

In the foreground you get a Drizzle-style typed query builder plus useQuery / useInfiniteQuery hooks that re-render automatically whenever a table changes — no matter whether the write came from your own code, raw SQL, a migration, or the background sync engine.

Why native sync matters

A sync engine implemented in JS has to run inside a headless JS task to do anything in the background — on iOS that means bridging BGTaskScheduler callbacks into a JS context that may not even be alive, and on Android a WorkManager job that has to boot a full JS runtime before it can make a single HTTP request. Both cost real time and battery just to get the JS engine started, and both are subject to whatever the OS decides about JS-thread scheduling under background execution limits — a class of reliability bug that Salve DB simply doesn't have.

Salve DB's sync orchestrator, HTTP client, credential provider, and background scheduler live entirely in C++/Swift/Kotlin:

  • Background reliability — the OS wakes native code directly (SyncNativeEntryPoint); there's no JS bundle to load, no bridge to reconnect, no headless task that can be killed mid-sync because the JS runtime took too long to initialize.
  • Battery — no JS VM startup, no bridge serialization for every queued operation — the sync loop reads SQLite, executes HTTP requests, and applies results, all in native code.
  • Correctness under cold start — if the OS kills your app entirely and later wakes the background job in a fresh process, the native core rehydrates its state from a small persisted config file and runs the sync pass with no JS involved at all.

In the foreground, the same orchestrator is reachable from JS via Database.sync() / Database.syncAll(), and every local write — whether from your query builder, raw SQL, a migration, or sync itself — goes through the same SQLite triggers into one sync_queue, so there's no separate in-JS bookkeeping to keep in sync with the native state.

Architecture at a glance

TypeScript (DX layer) → JSI (Nitro Modules) → Native Core (C++) → Swift (iOS) / Kotlin (Android)
  • TypeScriptDatabase.configure/register, the query builder (select/insert/update/ delete/transaction), useQuery/useInfiniteQuery, <SalveDbProvider>.
  • JSI (Nitro Modules) — a zero-copy bridge between JS and the native core; foreground calls cross it synchronously, and native change events cross it back to trigger React re-renders.
  • Native Core (C++) — SQLite execution with a prepared-statement cache, the migration engine, the trigger engine that populates sync_queue, and the sync orchestrator.
  • Swift / Kotlin edges — the platform APIs the C++ core can't reach directly: BGTaskScheduler / WorkManager for background scheduling, URLSession / OkHttp for HTTP, Keychain / Keystore for OAuth2 token storage.

See Architecture for the full breakdown of each layer.

What's next

Head to Installation to add Salve DB to your app, then follow the Quick Start to define a schema and run your first query.