04 — Offline-first storage and synchronization for Rasd Forms (React web/PWA + React Native)
Research date: 2026-08-15. Scope: local storage engines for web (IndexedDB / OPFS / SQLite-WASM) and React Native (SQLite / MMKV / secure store), hosted and self-hosted sync engines, sync-protocol design (outbox, idempotency, backoff, resumable attachment uploads, conflict strategy per data class), background execution, encryption at rest, and the shape of a storage-adapter abstraction. All version numbers, prices and dates below were checked against primary pages on 2026-08-15 unless marked "unverified".
Summary
- Web: IndexedDB is still the pragmatic default. Dexie 4.4.1 (Apache-2.0, released 2026-03-27) is the most complete wrapper and now offloads Blobs >= 4 KB / strings > 32 KB during Dexie Cloud sync; RxDB's core is Apache-2.0 but its IndexedDB, OPFS, SQLite and encryption storages are paid ("Pro from $99/month, billed annually") [1][2][5].
- SQLite-in-the-browser is viable for large local datasets (100 MB–1 GB+) via wa-sqlite
OPFSCoopSyncVFS, but costs ~0.9 MB of WASM, needs a Worker, has Safari-private-mode and iOS-WebView caveats, and small-write latency is ~10x worse than plain IndexedDB (1.5 ms vs 0.17 ms per write in RxDB's July 2026 micro-benchmark) [7][10]. - Quotas are generous (Chromium 60 % of disk; Firefox 10 %/10 GiB best-effort, 50 % persistent; Safari ~60 % per origin, 15 % inside third-party WKWebViews) but eviction is all-or-nothing per origin and Safari still deletes all script-writable storage after 7 days of Safari use without interaction on the site — unless the app is installed to the Home Screen [11][12].
- React Native:
react-native-quick-sqliteis deprecated (last 8.2.7); its successors arereact-native-nitro-sqlite(v9+, MIT, needs RN 0.75+ and Nitro) andop-sqlite(MIT; SQLCipher, libSQL/Turso, FTS5, reactive queries).expo-sqlite(SDK 57 docs) now has SQLCipher viauseSQLCipher, a Session/Changeset API,kv-store, and alpha web support requiring COOP/COEP headers [16][20][21][22]. - MongoDB shut down Atlas Device Sync on 2025-09-30; Realm continues only as a community local database. WatermelonDB's changelog stops at 0.27.1 (2023-10-15). Neither should be a foundation for a new library [24][26][27][28].
- Hosted sync engines diverge sharply on offline writes: PowerSync (Free / Pro from $49 / Team from $599; free source-available Open Edition) and Turso (
@tursodatabase/sync-react-native, GA Jan 2026) support offline writes; ElectricSQL 1.0 (Apache-2.0, cloud $1 per 1M writes) is read-path only; Zero explicitly rejects offline writes [29][30][32][34]. - No engine fits a form-builder library's data model out of the box. Submissions are append-mostly (an outbox with idempotency keys is enough); form definitions are collaboratively edited (a CRDT — Yjs, Loro or Automerge — or server-side revision/rebase); longitudinal case records need field-level LWW or a manual conflict queue [39][40][42].
- Attachments (photos, audio, signatures) should be uploaded resumably. tus 1.0.0 (2016) is the de-facto protocol and is being standardized as IETF
draft-ietf-httpbis-resumable-upload-12(2026-07-06); ODK Central accepts submissions split across multiple POSTs and advertisesX-OpenRosa-Accept-Content-Length: 100 MB[35][36][38]. - Background execution cannot be relied on: Background Sync API is Chromium-only (~76.7 % global, no Safari/Firefox, caniuse July 2026); on RN
expo-background-task(WorkManager / BGTaskScheduler) allows a 15-minute minimum interval but the OS decides when — sync must be foreground-driven with opportunistic background top-ups [15][18][19]. - Encryption at rest is cheap and expected by UN/NGO security reviews: SQLCipher (BSD-style community licence with attribution) via
expo-sqlite/op-sqlite, MMKV v4 AES-256,expo-secure-storefor the wrapping key (values <= ~2 KB), and WebCrypto AES-GCM with a non-extractableCryptoKeypersisted in IndexedDB on the web [16][17][25][43][44].
1. Web storage engines
1.1 IndexedDB wrappers
Dexie.js. Version 4.4.1 (with dexie-cloud-addon@4.4.7) shipped 2026-03-27 [1]. Relevant to a forms library: it uses IndexedDB 3.0 getAll(options) for reverse-ordered range queries, adds FinalizationRegistry-based collection of unclosed instances (maxConnections, default 1000), and — for Dexie Cloud users — automatically offloads any Blob/File/ArrayBuffer >= 4 KB and strings > 32 KB to blob storage, with eager/lazy fetch modes and a db.cloud.blobProgress observable [1]. Yjs support was moved out of core into a separate y-dexie addon; dexie-cloud-addon extends DexieYProvider for sync + awareness [4]. Dexie the library is Apache-2.0. Dexie Cloud pricing: Free tier (3 production users, 50,000 evaluation users, 100 MB, 10 databases, 20 req/s); Pro €0.12 per user/month with storage scaling by seats (25 seats = 1 GB object + 20 GB blob); on-prem Business €3,495 one-off (5 years of updates) and Enterprise €7,995 with full source [2]. Dexie's own guidance on media: store Blobs in IndexedDB but never index them — indexed binary makes apps "slower and finally crash"; index a hash or tags instead, and consider a ~2000-byte limit on indexed values [3].
RxDB. Core is Apache-2.0 and free storages include Memory, LocalStorage and Dexie.js. The IndexedDB, OPFS, SQLite (RN/Capacitor/Node), Expo-Filesystem, Worker, Sharding, Memory-Mapped and WebCrypto-encryption plugins are premium: Pro "from $99/month" and Pro Plus "from $239/month", both billed annually with unlimited developers, plus a perpetual option and no trial [5][6]. That licence model is incompatible with an open-core library that itself sells a subscription — RxDB could only be an optional adapter that customers licence themselves.
PouchDB. 9.0.0 (blog dated 2024-05-24; GitHub release entry dated 2024-06-21) rewrote the indexeddb adapter and introduced a default .find() limit of 25. No later release appears on the releases page as of 2026-08-15, and the project is in Apache incubation [9]. CouchDB replication is mature but the document/revision-tree model, attachment handling and per-document conflict _revs are heavier than a forms outbox needs.
idb / TinyBase. Not re-verified in this pass. idb is a thin promise wrapper (a few KB); TinyBase is an in-memory reactive store with IndexedDB/SQLite persisters. Treat both as candidates for the low-level adapter, not as sync solutions.
1.2 SQLite in the browser (WASM + OPFS)
PowerSync's "State of SQLite persistence on the web" (updated 2026-05-15) is the best current field report [10]: IDBBatchAtomicVFS (IndexedDB-backed, needs Asyncify/JSPI) is fine for small databases but degrades above ~100 MB and can stack-overflow on Safari with large queries; wa-sqlite OPFSCoopSyncVFS "keeps performing well even for databases over 1GB" and needs no Asyncify; the official @sqlite.org/sqlite-wasm opfs VFS requires SharedArrayBuffer (COOP/COEP) while opfs-sahpool is faster but takes an exclusive lock; a new OPFSWriteAheadVFS (April 2026) allows concurrent reads during writes on Chrome 121+. Caveats: Safari private mode has no OPFS (fall back to IndexedDB); Chrome incognito caps SQLite databases around 100 MB; SharedWorkers cannot open OPFS; suspended tabs close access handles (mitigate with Web Locks); iOS WKWebView (Capacitor) closes OPFS handles when backgrounded [10].
Micro-benchmarks (RxDB, 2026-07-21, Chrome desktop) [7]: small write latency LocalStorage 0.017 ms, IndexedDB 0.17 ms, OPFS worker 1.54 ms, WASM-SQLite-on-IndexedDB 3.17 ms; small read IndexedDB 0.10 ms vs OPFS 1.4 ms; 200-doc bulk write IndexedDB 13.4 ms vs OPFS worker 104 ms; init IndexedDB 46 ms vs WASM SQLite 535 ms; the SQLite WASM bundle is ~939 kB. Expo's own web build of expo-sqlite is alpha and needs Cross-Origin-Embedder-Policy: credentialless + Cross-Origin-Opener-Policy: same-origin [16] — headers many NGO hosting setups (SharePoint, generic PHP hosts) cannot set.
Conclusion for Rasd Forms: IndexedDB (via Dexie or a small custom wrapper) as the default web adapter; SQLite-WASM/OPFS as an optional adapter for deployments that need SQL-style querying over tens of thousands of submissions.
1.3 Quotas, persistence, eviction, private mode, blobs
MDN (updated 2026-01-05) [11]: Chromium grants 60 % of total disk per origin (best-effort and persistent alike), evicting best-effort origins LRU when the browser exceeds 80 % of disk; Firefox best-effort is min(10 % of disk, 10 GiB per site group), persistent up to 50 % (8 TiB cap) with a visible permission prompt; Safari 17+/macOS 14+ gives ~60 % per origin in the browser, but only ~15 % per origin (20 % overall) to WKWebView-embedded content unless installed as a web app; pre-iOS-17 Safari started at 1 GiB with prompts. Eviction is all-or-nothing per origin. navigator.storage.persist() is silent in Chromium and Safari (Chrome heuristics: engagement, bookmark/PWA install, notification permission [13]) and prompts in Firefox; estimate() returns padded, disk-size-based figures. Safari's ITP deletes all script-writable storage (IndexedDB, localStorage, Cache, SW registrations) after 7 days of Safari use without interaction on the site; Home Screen web apps keep their own counter and are exempt [12]. Firefox 115 (2023-07-04) enabled IndexedDB in Private Browsing with encrypted on-disk storage [14]; Chrome incognito uses a reduced quota wiped on close [10][11]. RxDB's practical numbers for mobile: iOS Safari "around 1 GB" historically, Firefox mobile ~5 MB initial with prompts [8] — treat these as lower bounds you must test.
2. React Native storage engines
| Library | Version / status (2026-08-15) | Licence | Notes |
|---|---|---|---|
expo-sqlite | SDK 57 docs; openDatabaseAsync/Sync, prepared statements, withTransactionAsync, withExclusiveTransactionAsync, createSessionAsync/createChangesetAsync/invertChangesetAsync, addDatabaseChangeListener, serializeAsync, backupDatabaseAsync, expo-sqlite/kv-store, expo-sqlite/localStorage/install; config plugin useSQLCipher, useLibSQL, enableFTS (default true), withSQLiteVecExtension, customBuildFlags; web alpha (COOP/COEP) [16] | MIT | SQLCipher not in Expo Go; Drizzle ORM support |
op-sqlite | Active; JSI; SQLCipher build flag, libSQL/Turso embedded replicas, FTS5, R-tree, sqlite-vec, reactive queries, runtime extension loading, JSONB, db.interrupt(), built-in KV [20] | MIT | Broadest feature set; not Nitro-based |
react-native-nitro-sqlite | v9+, requires react-native-nitro-modules and RN 0.75+; sync + async, async-only transactions, batch, attach DBs, sqlite-vec optional; TypeORM driver [21] | MIT | Successor to react-native-quick-sqlite (deprecated, last 8.2.7) [22]; SQLCipher not documented |
| WatermelonDB | 0.27.1, 2023-10-15 last changelog entry; own sync protocol (pull/push, experimentalStrategy: 'replacement') [24] | MIT | Lazy-loading ORM; stale releases; New-Architecture status unclear |
react-native-mmkv | v4.3.2 (2026-06-22); Nitro rewrite; AES-256 encrypt()/decrypt(), recrypt(), isEncrypted; RN 0.87 support [25] | MIT | Key-value only — settings, sync cursors, licence token cache |
| Realm / Atlas Device Sync | Device Sync shut down 2025-09-30; SDKs in "keep the lights on" mode; community realm-js without sync [26][27][28] | Apache-2.0 | Do not adopt |
expo-secure-store | SDK 57; iOS Keychain kSecClassGenericPassword, Android Keystore-encrypted SharedPreferences; values above ~2048 bytes may be rejected; requireAuthentication biometrics; keychainAccessible e.g. WHEN_UNLOCKED_THIS_DEVICE_ONLY; no web [17] | MIT | Store keys, not data; note iOS values survive uninstall |
Performance: PowerSync's July 2024 benchmark (iPhone 15 Pro, Samsung S22) found op-sqlite, quick-sqlite and expo-sqlite similar on iOS and device-dependent on Android, with transactions/batching and WAL mode making the biggest difference [23]. Batching, WAL and prepared statements matter more than the binding choice for low-end Android; keep the abstraction so customers can pick the binding.
3. Sync engines and services
| Engine | Model | Offline writes | Price / licence (verified) |
|---|---|---|---|
| PowerSync | Postgres/MongoDB/MySQL/SQL Server -> client SQLite (RN, web, Flutter, Kotlin, Swift) | Yes (upload queue) | Cloud Free $0 (2 GB synced/mo, 500 MB hosted, 50 connections, projects sleep after 1 week); Pro from $49/mo (30 GB, 1,000 connections, then $1/GB, $30 per 1,000 connections); Team from $599; Enterprise custom. Open Edition self-hosted, free, source-available [29] |
| ElectricSQL | Postgres logical replication -> HTTP "shapes"; TanStack DB collections | Read-path only; writes are your API | Apache-2.0 self-host; Cloud PAYG $1 per 1M writes + $0.10/GB-month retention, reads free, <$5/mo waived; Pro $249/mo; Scale $1,999/mo [30][31] |
| Zero (Rocicorp) | Postgres + zero-cache | No — "writes are rejected and return an offline error"; brief queueing only while connecting [32] | Not verified |
| Turso / libSQL | Embedded replica SQLite <-> Turso Cloud; @tursodatabase/sync-react-native (2026-01-29, RN 0.76+ New Arch) | Yes; encryption at rest; partial sync and Expo plugin still "planned" [34]; March 2025 beta had conflict detection only [33] | Not verified for 2026 |
| Dexie Cloud | Dexie tables + Yjs docs -> Dexie Cloud server | Yes | Free tier; €0.12/user/mo; on-prem €3,495 / €7,995 [2] |
| TanStack DB 0.6 | Client store with persistence + offline transaction API; adapters for Electric, PowerSync, RxDB | Yes via adapters | MIT (unverified) [46] |
None of these owns the form semantics (repeat groups, skip logic, attachments, enumerator/supervisor roles), and several introduce a mandatory backend that many UN agencies cannot deploy (Postgres logical replication, a hosted service). PowerSync's Open Edition and Electric's Apache-2.0 server are the two that can be self-hosted freely; both are worth documenting as optional transports rather than hard dependencies.
4. Sync-protocol design for Rasd Forms
4.1 Which consistency model per data class
| Data class | Write pattern | Recommended model | Why |
|---|---|---|---|
| (a) Submissions / instances | Append-mostly; edited by one enumerator until submitted; later reviewed server-side | Transactional outbox + server idempotency key (submissionId UUIDv7 + attempt), last-write-wins on server-side status fields only | No concurrent editing; conflicts are almost always duplicates from retries, which idempotency removes |
| (b) Form definitions (builder) | Low volume, concurrent editing by a few designers, needs undo/versions | CRDT document (Yjs, Loro or Automerge) or optimistic-lock revisions with server rebase and explicit publish | Structural JSON (question tree, choice lists, translations) merges well as CRDT maps/arrays; publish is a snapshot with version and content hash |
| (c) Longitudinal case records (household, beneficiary, site) | Multiple monitors update the same record over months, offline | Field-level LWW with hybrid-logical-clock timestamps + tombstones, plus a "needs review" conflict queue when two devices changed the same field | Full CRDT is overkill and hard to explain to M&E staff; field-level merge preserves both edits except true collisions |
CRDT facts for (b): in crdt-benchmarks B4 (260 K real edits, ~105 K chars) document sizes were Yjs 13.6.15 227 KB, Loro 1.0-beta 231 KB, Automerge 2.1.10 129 KB; parse time Yjs 27 ms, Loro 6 ms, Automerge 1,185 ms; at B4x100 Yjs used 314 MB RAM to parse vs Loro 1.6 kB [39]. Automerge 3.0 (July 2025, @automerge/automerge 3.0, automerge-repo 2.1.0) cut memory "over 10x" (700 MB -> 1.3 MB for a Moby-Dick doc) while keeping the v2 file format [40]. loro-crdt is at 1.13.x on npm, MIT [41]. Yjs has y-indexeddb for offline persistence and y-dexie for Dexie integration [4][42]. A form definition is a few hundred KB at most, so all three are fine; Yjs has the widest ecosystem, Loro the smallest/fastest WASM path, Automerge the richest history/branching model.
4.2 Outbox, idempotency, retries
- Write the submission row and an
outboxrow in the same local transaction; a single "sync worker" drains the outbox FIFO per form (ordering matters for edits of the same submission), markssent_at, and moves permanent failures (4xx other than 408/425/429) to a dead-letter state visible in the UI. - Idempotency: server keys on
(tenant, submissionId, revision); the client sendsIdempotency-Keyon every POST/PATCH so replays after a dropped response are no-ops. Include adeviceId,clientCreatedAt(device clock) andhlc(hybrid logical clock) to allow ordering that survives wrong device clocks — endemic on shared field phones. - Backoff:
delay = base * 2^n +/- jitter, capped (e.g. 1 s -> 5 min), reset on any successful call; retry immediately ononline/visibilitychange/app-foreground events; honorRetry-After. - Delta pull: forms, choice lists and case records are pulled with a per-collection cursor (server sequence or
updatedAt+ tie-breaker id), paged, and applied in a transaction; areplacementfull-resync mode (as WatermelonDB offers [24]) is needed to recover corrupted devices. - Multi-tab web: elect a single syncing tab with
navigator.locks.request('rasd-sync', ...)and fan out change events overBroadcastChannel[45].
4.3 Attachments
Photos are the bulk of bytes and the main failure point on 2G/3G. Store them as Blobs (web) or files (RN FileSystem) referenced by content hash; never index the bytes [3]. Upload separately from the JSON payload using tus 1.0.0: POST (Creation), PATCH with Content-Type: application/offset+octet-stream at Upload-Offset, HEAD to resume, Upload-Metadata, optional Checksum (SHA-1 minimum), Expiration and Termination extensions [35]; tus-js-client runs in browsers, Node and React Native and recommends large or chunkless uploads for throughput [37]. The IETF successor, draft-ietf-httpbis-resumable-upload-12 (2026-07-06, Upload-Complete, Upload-Offset, 104 Upload Resumption Supported, application/partial-upload) is on the standards track and largely tus-compatible [36]. For OpenRosa/ODK-compatible back ends, submissions may be split across multiple POSTs and ODK Central advertises X-OpenRosa-Accept-Content-Length = 100 MB (Collect historically used 10 MB parts) [38]. Design the server-facing contract so an attachment can be uploaded before, after, or in parallel with its submission, and the submission is only "complete" when all referenced hashes exist server-side.
4.4 Conflict-resolution UX
Enumerators must never see a merge dialog in the field. Submissions: no UX (idempotent). Case records: apply field-level merge silently; only true same-field collisions land in a supervisor "Conflicts" list showing both values, device, time and monitor, with one-tap "keep mine / keep theirs / merge". Form definitions: CRDT merges silently in the builder; publishing is an explicit action that fixes a version number, and devices with in-flight instances of the older version keep that schema snapshot locally.
5. Background execution
Web: Background Sync API is supported in Chrome 49+, Edge 79+, Opera, Samsung Internet and Android Chrome, but no Firefox or Safari (any version) — 76.7 % global as of caniuse July 2026; Periodic Background Sync is Chromium-only too [15]. Use it opportunistically (registration.sync.register('rasd-outbox')) but drive sync from the foreground: on online, visibilitychange, app start, and after every submit. Service-worker fetches must not depend on main-thread state.
React Native: expo-background-task (SDK 53+, replaces deprecated expo-background-fetch) uses WorkManager on Android and BGTaskScheduler on iOS; minimumInterval defaults to 12 hours, minimum 15 minutes, iOS needs UIBackgroundModes: ["processing"] and BGTaskSchedulerPermittedIdentifiers; tasks are skipped on low battery/no network, do not run on the iOS simulator, stop if the user force-quits the app, and there is no guarantee of when they run [18][19]. Same conclusion: foreground-first sync, background as a bonus, and expose "Last synced / N pending" prominently.
6. Encryption at rest, keys, integrity
- Native: SQLCipher via
expo-sqliteuseSQLCipher: true(not in Expo Go) [16] orop-sqlite's SQLCipher build [20]. SQLCipher Community Edition is BSD-style but requires reproducing the licence/copyright in a user-accessible place (about screen or docs) [43]; ship that notice with the library. MMKV v4 provides AES-256 for small key-value data [25]. - Key material: generate a random 256-bit DB key on first launch, store it in
expo-secure-store(Keychain / Keystore; keep under ~2 KB; chooseWHEN_UNLOCKED_THIS_DEVICE_ONLYso keys do not migrate in iCloud backups) [17]. Optionally wrap the key with a passphrase-derived key (PBKDF2/Argon2) for "device shared between enumerators" deployments. - Web:
crypto.subtle.generateKey({name:'AES-GCM', length:256}, false, ['encrypt','decrypt'])yields a non-extractableCryptoKey(extractable: falseblocksexportKey/wrapKey) that can be stored as a structured-clone value in IndexedDB and used to encrypt record payloads and blobs with a random 96-bit IV per record [44]. This defends against casual disk inspection and cross-app leakage but not against a script running in the same origin; document that honestly. Encrypted values cannot be indexed, so keep indexable metadata (formId, status, updatedAt) in cleartext columns and encrypt only answer payloads and media. - Integrity: store SHA-256 of each serialized submission and of each attachment; send it in the payload and as tus Checksum; server verifies and returns the hash in the ack so the client can prove delivery. Sign the form definition (JWS over the canonical JSON) so devices can verify a published form came from the tenant's server.
7. Storage abstraction layer
The library should ship one interface and several adapters:
interface RasdStorageAdapter {
open(opts: { name: string; encryptionKey?: KeyRef }): Promise<void>;
transaction<T>(mode: 'r' | 'rw', scopes: Collection[], fn: (tx) => Promise<T>): Promise<T>;
put(c: Collection, rows: Row[]): Promise<void>;
get(c: Collection, id: string): Promise<Row | undefined>;
query(c: Collection, q: { index: string; range?: KeyRange; limit?: number; reverse?: boolean }): AsyncIterable<Row>;
putBlob(hash: string, data: Blob | Uint8Array | FileRef): Promise<void>;
getBlob(hash: string): Promise<Blob | FileRef | undefined>;
outbox: { enqueue(op: SyncOp): Promise<void>; peek(n: number): Promise<SyncOp[]>; ack(ids: string[]): Promise<void>; nack(id: string, err: SyncError): Promise<void> };
cursors: { get(collection: string): Promise<string | undefined>; set(collection: string, cursor: string): Promise<void> };
estimate(): Promise<{ usage: number; quota: number; persisted: boolean }>;
subscribe(c: Collection, cb: (change: Change) => void): () => void;
migrate(to: number, steps: Migration[]): Promise<void>;
close(): Promise<void>;
}
Adapters: @rasd/storage-indexeddb (Dexie or hand-rolled, blobs as unindexed Blob values, key store for the AES key), @rasd/storage-sqlite-web (wa-sqlite OPFSCoopSyncVFS in a Worker, IndexedDB VFS fallback for Safari private mode) [10], @rasd/storage-expo-sqlite, @rasd/storage-op-sqlite, @rasd/storage-nitro-sqlite, @rasd/storage-memory (tests/SSR). Sync cursors, licence-token cache and settings can go to MMKV/localStorage shims but must be replicated into the main store so a single backup/export is complete. Keep query needs to a small indexed subset (formId, status, updatedAt, caseId) so both IndexedDB indices and SQLite indices satisfy them without a query planner.
Comparison table — web storage engines
| Option | Licence | Cost | Blob handling | Multi-tab | Safari private | Bundle | Verdict |
|---|---|---|---|---|---|---|---|
| Dexie 4.4.1 | Apache-2.0 | free (Cloud optional) | Blobs OK, don't index [3]; Cloud offloads >= 4 KB [1] | Dexie liveQuery/BroadcastChannel | works (in-memory) | small | Default adapter |
| RxDB (Dexie storage) | Apache-2.0 core | IndexedDB/OPFS/SQLite/crypto plugins $99+/mo annual [5] | via attachments plugin | leader election built-in | works | larger | Optional, customer-licensed |
| PouchDB 9.0.0 | Apache-2.0 | free | attachments in doc model | yes | works | ~ (unverified) | Only for CouchDB shops |
| wa-sqlite / sqlite-wasm + OPFS | MIT / public domain | free | files in OPFS | Web Locks required [10] | no OPFS -> IDB VFS fallback [10] | ~0.9 MB [7] | Optional adapter for large datasets |
Implications & recommendations for Rasd Forms
- Ship IndexedDB (Dexie 4.4.x or a purpose-built wrapper) as the default web adapter and expo-sqlite as the default RN adapter; add op-sqlite and nitro-sqlite adapters and a wa-sqlite/OPFS web adapter behind the same
RasdStorageAdapterinterface. Do not depend on RxDB premium, Realm or WatermelonDB. - Model three data classes explicitly in the schema and sync engine:
submissions(outbox + idempotency, no merge),forms(CRDT doc or revision+rebase, explicit publish, immutable version snapshots on devices),cases(field-level LWW with HLC and a supervisor conflict queue). Start with Yjs for the builder (largest ecosystem, y-indexeddb, y-dexie); keep the CRDT behind an interface so Loro can be swapped in for size/perf. - Make attachments first-class: content-hash addressed, stored unindexed, uploaded with tus (and, when the IETF spec lands, the same code path), resumable across app restarts, with per-attachment progress and a "pending uploads" counter; support OpenRosa-style multi-POST submission for ODK/Kobo back ends.
- Sync is foreground-driven with a single leader (Web Locks on web, one JS worker on RN), event-triggered (online, foreground, submit) with jittered exponential backoff; Background Sync API and expo-background-task are opportunistic add-ons only, and the UI must always show last-sync time and pending counts.
- On web, call
navigator.storage.persist()at first successful login, surface the result, and push installation to the Home Screen in Safari to escape the 7-day ITP purge; warn whenestimate()shows < 100 MB headroom or when running in private mode; detect and message quota errors as "cannot save offline" rather than crashing. - Provide encryption at rest as a one-line option: SQLCipher on native (ship the SQLCipher attribution text), WebCrypto AES-GCM with a non-extractable key on web; keys in
expo-secure-store(<= 2 KB,WHEN_UNLOCKED_THIS_DEVICE_ONLY) and never in MMKV/localStorage; encrypt payloads and media, keep index columns cleartext. - Add integrity end-to-end: SHA-256 per submission and attachment sent with the payload and echoed in the ack; JWS-signed published form definitions.
- Budget for low-end Android: batch writes in transactions, WAL mode, prepared statements, avoid loading whole result sets (
getEachAsync), keep the web bundle small (no WASM SQLite in the default path), and provide areplacementfull-resync escape hatch. - Document (and test in CI on real devices/browsers) the known platform traps: Safari private mode (no OPFS), Chrome incognito quota, iOS WKWebView 15 % quota, Firefox persist prompt, expo-sqlite web COOP/COEP headers, expo-background-task not running on simulators.
- Position optional integrations, not dependencies, for PowerSync (Open Edition self-host; Pro from $49/mo), ElectricSQL (Apache-2.0; read-path), Turso sync (RN GA Jan 2026), Dexie Cloud (€0.12/user/mo) — each as a documented "transport" behind the sync interface, so UN agencies with existing Postgres/CouchDB/ODK stacks can plug in.
Sources
- Dexie 4.4 & Dexie Cloud Server 3.0 (2026-03-27) — https://medium.com/dexie-js/dexie-4-4-dexie-cloud-server-3-0-the-big-one-d883b98599e8 (accessed 2026-08-15)
- Dexie Cloud pricing — https://dexie.org/pricing (accessed 2026-08-15)
- Dexie: keep storing large images, just don't index the binary data — https://medium.com/dexie-js/keep-storing-large-images-just-dont-index-the-binary-data-itself-10b9d9c5c5d7 (accessed 2026-08-15)
- Dexie issue #2114 "Break out Y.js support into its own addon" / y-dexie — https://github.com/dexie/Dexie.js/issues/2114 (accessed 2026-08-15)
- RxDB premium / pricing — https://rxdb.info/premium/ (accessed 2026-08-15)
- RxDB RxStorage overview — https://rxdb.info/rx-storage.html (accessed 2026-08-15)
- RxDB: LocalStorage vs IndexedDB vs Cookies vs OPFS vs WASM-SQLite (2026-07-21) — https://rxdb.info/articles/localstorage-indexeddb-cookies-opfs-sqlite-wasm.html (accessed 2026-08-15)
- RxDB: IndexedDB max storage size limit — https://rxdb.info/articles/indexeddb-max-storage-limit.html (accessed 2026-08-15)
- PouchDB releases / 9.0.0 — https://github.com/pouchdb/pouchdb/releases and https://pouchdb.com/2024/05/24/pouchdb-9.0.0.html (accessed 2026-08-15)
- PowerSync: The current state of SQLite persistence on the web (updated 2026-05-15) — https://powersync.com/blog/sqlite-persistence-on-the-web (accessed 2026-08-15)
- MDN: Storage quotas and eviction criteria (updated 2026-01-05) — https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria (accessed 2026-08-15)
- WebKit: Full third-party cookie blocking and more (7-day cap, 2020-03-24) — https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ (accessed 2026-08-15)
- web.dev: Persistent storage — https://web.dev/articles/persistent-storage (accessed 2026-08-15)
- Firefox 115 release notes (IndexedDB in Private Browsing) — https://www.firefox.com/en-US/firefox/115.0/releasenotes/ (accessed 2026-08-15)
- caniuse: Background Sync API — https://caniuse.com/background-sync (accessed 2026-08-15)
- Expo SQLite docs (SDK 57) — https://docs.expo.dev/versions/latest/sdk/sqlite/ (accessed 2026-08-15)
- Expo SecureStore docs (SDK 57) — https://docs.expo.dev/versions/latest/sdk/securestore/ (accessed 2026-08-15)
- Expo BackgroundTask docs (SDK 57) — https://docs.expo.dev/versions/latest/sdk/background-task/ (accessed 2026-08-15)
- Expo blog: Goodbye background-fetch, hello expo-background-task — https://expo.dev/blog/goodbye-background-fetch-hello-expo-background-task (accessed 2026-08-15)
- op-sqlite README — https://github.com/OP-Engineering/op-sqlite (accessed 2026-08-15)
- react-native-nitro-sqlite README — https://github.com/margelo/react-native-nitro-sqlite (accessed 2026-08-15)
- react-native-quick-sqlite (DEPRECATED) — https://github.com/margelo/react-native-quick-sqlite (accessed 2026-08-15)
- PowerSync: React Native database performance comparison (2024-07) — https://powersync.com/blog/react-native-database-performance-comparison (accessed 2026-08-15)
- WatermelonDB changelog — https://watermelondb.dev/docs/CHANGELOG (accessed 2026-08-15)
- react-native-mmkv releases (v4.3.2, 2026-06-22) — https://github.com/mrousavy/react-native-mmkv/releases (accessed 2026-08-15)
- MongoDB forum: Atlas Device Sync end-of-life and deprecation — https://www.mongodb.com/community/forums/t/atlas-device-sync-end-of-life-and-deprecation/296687 (accessed 2026-08-15; page returned 403 to the fetcher, summary via search snippet)
- realm-js discussion #6884: Device Sync deprecation — https://github.com/realm/realm-js/discussions/6884 (accessed 2026-08-15)
- Couchbase blog: MongoDB ends mobile support (2025-09-30) — https://www.couchbase.com/blog/realm-mongodb-eol-day-2025/ (accessed 2026-08-15)
- PowerSync pricing — https://www.powersync.com/pricing (accessed 2026-08-15)
- Electric Cloud pricing — https://electric.ax/pricing (accessed 2026-08-15)
- Electric 1.0 released (2025-03-17) — https://electric-sql.com/blog/2025/03/17/electricsql-1.0-released (accessed 2026-08-15)
- Zero docs: Offline — https://zero.rocicorp.dev/docs/offline (accessed 2026-08-15)
- Turso: Offline Sync public beta (2025-03-31) — https://turso.tech/blog/turso-offline-sync-public-beta (accessed 2026-08-15)
- Turso: React Native bindings (2026-01-29) — https://turso.tech/blog/react-native-bindings-for-turso (accessed 2026-08-15)
- tus resumable upload protocol 1.0.0 — https://tus.io/protocols/resumable-upload (accessed 2026-08-15)
- IETF draft-ietf-httpbis-resumable-upload-12 (2026-07-06) — https://datatracker.ietf.org/doc/draft-ietf-httpbis-resumable-upload/ (accessed 2026-08-15)
- tus-js-client — https://github.com/tus/tus-js-client (accessed 2026-08-15)
- ODK OpenRosa Form Submission API and Collect issue #2012 — https://docs.getodk.org/openrosa-form-submission/ , https://github.com/opendatakit/collect/issues/2012 (accessed 2026-08-15)
- crdt-benchmarks (Yjs / Loro / Automerge B4) — https://github.com/zxch3n/crdt-benchmarks (accessed 2026-08-15)
- Automerge 3.0 announcement (2025-07) — https://automerge.org/blog/automerge-3/ (accessed 2026-08-15)
- loro-crdt on npm — https://www.npmjs.com/package/loro-crdt (accessed 2026-08-15)
- Yjs docs: offline support / y-indexeddb — https://docs.yjs.dev/getting-started/allowing-offline-editing (accessed 2026-08-15)
- SQLCipher licence (Zetetic) — https://www.zetetic.net/sqlcipher/license/ (accessed 2026-08-15)
- MDN: CryptoKey.extractable — https://developer.mozilla.org/en-US/docs/Web/API/CryptoKey/extractable (accessed 2026-08-15)
- MDN: Web Locks API — https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API (accessed 2026-08-15)
- TanStack DB 0.6: persistence and offline — https://tanstack.com/blog/tanstack-db-0.6-app-ready-with-persistence-and-includes (accessed 2026-08-15)
- Expo SDK 53 changelog — https://expo.dev/changelog/sdk-53 (accessed 2026-08-15)