Expo and browser storage
Use Expo when the database belongs to the app: offline documents and queries stay in local SQLite while your application keeps the same asynchronous API as the server. This page assumes Silt is installed from a local package or a published release; see Quick start. Install the optional expo-sqlite peer using the Expo version compatible with your app.
@silt-db/sqlite resolves to the async Expo entry under Metro's react-native and browser conditions. The explicit @silt-db/sqlite/expo import is also available. Initialization is asynchronous on every platform:
Use the transaction-scoped tx inside a transaction callback. Other calls on the parent database queue behind that transaction, so an unrelated write cannot accidentally become part of a rollback. db.driver exposes async SQL operations for adapters and diagnostics. No JavaScript document-query evaluator or custom SQLite extension is installed.
Reproduce the integration
The runnable example is integration/expo. Its runtime versions are Expo 57.0.20, expo-sqlite 57.0.2, React 19.2.3, and React Native 0.86.3. It imports the actual pinned Teamplay source and the same application module used by the Node integration matrix.
TEAMPLAY_CHECKOUT may point to the pinned upstream checkout described in TEAMPLAY.md. Metro watches that directory, selects Teamplay's teamplay-ts source condition, and resolves one shared React installation. Integration dependencies are isolated from the root workspace.
On Linux CI where the browser CDN is unavailable, the test can use the Chromium executable shipped in the pinned npm package:
The browser runner performs a fresh production Expo web export, serves it locally with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp, and launches a real Chromium browser with OPFS. It asserts crossOriginIsolated, SharedArrayBuffer, and OPFS availability before opening SQLite. No browser security-disabling flag is used. Metro includes the SQLite WASM asset and a real worker bundle. The runner uses the production server because setting only Metro's enhanceMiddleware did not apply those headers to the HTML response in this SDK's development server.
The Expo compiler/driver dependency graph has no Node APIs or Buffer dependency. The local ShareDB backend itself needs upstream stream, Buffer, and process compatibility packages; these are installed and initialized explicitly in the integration, not in Silt's shared SQL code.
Measured coverage
The captured evidence is in expo-validation-results.json. A fresh run writes integration/expo/artifacts/web-results.json, bundle-results.json, and a screenshot. The browser runner fails on missing prerequisites, failed assertions, uncaught browser exceptions, or subscription errors; it does not silently skip.
The runtime suite checks JSON types, Unicode, generated IDs, CRUD and updates, SQL math, JSON1, window functions, recursive CTEs, group plus lookup aggregation, a real SQLite query plan, persistent scalar/multikey indexes (including adjacent-double backfill and reload, plus hard-float backfill/trigger consistency against the same-engine unindexed scan), atomic batch rollback, explicit transaction rollback, 20 concurrent increments, and an outside write surviving a concurrent transaction rollback. Persistence is checked after connection close/reopen, full page reload, and closing the owner tab before a new tab opens the database.
The actual Teamplay scenario checks configured identity fields, sorted/limited subscriptions, a named lookup aggregation, live query reordering, foreign-collection changes refreshing lookup results, updates from a second ShareDB connection, delete propagation, and persistence of documents, queries, lookups, and tombstones across a fresh page. It then writes again. Backend teardown drains active agents and database callbacks before the borrowed SQLite database closes.
The host had Java but no adb, Android emulator, Gradle executable, or Xcode. Hermes export is JavaScript compilation, not an APK/IPA or execution of native expo-sqlite. On a configured machine, run:
The native app runs the same core and actual Teamplay validation modules automatically and prints SILT_EXPO_RESULT. Device restart persistence, background/foreground transitions, and platform-specific filesystem behavior still require those native runs.
Browser ownership and persistence limits
Expo documents web SQLite support as alpha. The SDK's actual AccessHandlePoolVFS retains synchronous OPFS handles for its worker. A second live worker at the same origin rejects with NoModificationAllowedError; this contention is broader than a single database filename. Closing a database connection flushes data but does not terminate that worker. Closing or navigating away from the owning tab releases its handles, after which a new tab can open persisted data. The raw-driver contention test verifies this behavior explicitly. The measured browser connection uses journal_mode=delete; requesting WAL does not establish WAL support in this VFS. Native WAL behavior is a separate platform check.
The example integration/teamplay/browser-owner.js makes this restriction usable across tabs. A Web Lock elects exactly one owner for the origin. That tab opens SQLite and runs the actual ShareDB backend; every tab sends the ShareDB JSON wire protocol through BroadcastChannel. The follower opens no database. The application still uses the same Teamplay $ and sub() calls.
The helper imports below refer to files in the repository, not package exports. Copy and adapt the browser owner helper with its Teamplay setup for an application.
The three-tab test verifies live document updates, sorted-query reordering, and foreign lookup refresh across tabs. It closes the owner page, verifies that the existing follower acquires ownership and reconnects its subscriptions, writes again, then opens a third tab and checks the durable result. Ownership lasts for the owner's page lifetime because the underlying OPFS pool keeps its handles. This bounded connector supports one application room and database owner per origin; it does not support several independent owner rooms competing for the same pool.
This is SQLite persistence inside the browser's origin storage. It does not turn browser storage into a remote backup or synchronize an offline database with a server. Browser profile deletion or origin-storage eviction has normal browser-storage consequences.
SQLite version and number rendering
The actual WASM version is 3.50.3, matching expo-sqlite's vendored native SQLite source; the native build files enable SQLite math functions. Compatibility probes run against the browser engine rather than assuming the Node SQLite version applies.
SQLite 3.50.3 can shorten a binary64 number when JSON functions or CAST(... AS TEXT) render it: json_array(1.0000000000000002) produces [1.0]. The captured probes also show differences in numeric formatting between SQLite versions. The compiler preserves adjacent doubles in the tested arithmetic, array, group, update, and index paths. Extreme decimal-to-binary conversion and formatting can still differ in this older WASM engine; the captured parser probes record those engine limitations explicitly, and exact cross-engine canonical numeric text must not be assumed. Numeric precision records the boundary and conservative handling. JSONB format probes were run unchanged on the actual WASM engine; the production document schema remains JSON text. See JSON storage experiment for the storage experiment and its limits.
References: Expo SQLite documentation, Expo Metro configuration. The installed SDK's expo-sqlite/web/wa-sqlite/AccessHandlePoolVFS.js, native build flags, and vendored sqlite3.h were also inspected directly.