إنتقل إلى المحتوى الرئيسي

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-sqlite is deprecated (last 8.2.7); its successors are react-native-nitro-sqlite (v9+, MIT, needs RN 0.75+ and Nitro) and op-sqlite (MIT; SQLCipher, libSQL/Turso, FTS5, reactive queries). expo-sqlite (SDK 57 docs) now has SQLCipher via useSQLCipher, 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 advertises X-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-store for the wrapping key (values <= ~2 KB), and WebCrypto AES-GCM with a non-extractable CryptoKey persisted 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

LibraryVersion / status (2026-08-15)LicenceNotes
expo-sqliteSDK 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]MITSQLCipher not in Expo Go; Drizzle ORM support
op-sqliteActive; 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]MITBroadest feature set; not Nitro-based
react-native-nitro-sqlitev9+, requires react-native-nitro-modules and RN 0.75+; sync + async, async-only transactions, batch, attach DBs, sqlite-vec optional; TypeORM driver [21]MITSuccessor to react-native-quick-sqlite (deprecated, last 8.2.7) [22]; SQLCipher not documented
WatermelonDB0.27.1, 2023-10-15 last changelog entry; own sync protocol (pull/push, experimentalStrategy: 'replacement') [24]MITLazy-loading ORM; stale releases; New-Architecture status unclear
react-native-mmkvv4.3.2 (2026-06-22); Nitro rewrite; AES-256 encrypt()/decrypt(), recrypt(), isEncrypted; RN 0.87 support [25]MITKey-value only — settings, sync cursors, licence token cache
Realm / Atlas Device SyncDevice Sync shut down 2025-09-30; SDKs in "keep the lights on" mode; community realm-js without sync [26][27][28]Apache-2.0Do not adopt
expo-secure-storeSDK 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]MITStore 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

EngineModelOffline writesPrice / licence (verified)
PowerSyncPostgres/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]
ElectricSQLPostgres logical replication -> HTTP "shapes"; TanStack DB collectionsRead-path only; writes are your APIApache-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-cacheNo — "writes are rejected and return an offline error"; brief queueing only while connecting [32]Not verified
Turso / libSQLEmbedded 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 CloudDexie tables + Yjs docs -> Dexie Cloud serverYesFree tier; €0.12/user/mo; on-prem €3,495 / €7,995 [2]
TanStack DB 0.6Client store with persistence + offline transaction API; adapters for Electric, PowerSync, RxDBYes via adaptersMIT (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 classWrite patternRecommended modelWhy
(a) Submissions / instancesAppend-mostly; edited by one enumerator until submitted; later reviewed server-sideTransactional outbox + server idempotency key (submissionId UUIDv7 + attempt), last-write-wins on server-side status fields onlyNo 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/versionsCRDT document (Yjs, Loro or Automerge) or optimistic-lock revisions with server rebase and explicit publishStructural 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, offlineField-level LWW with hybrid-logical-clock timestamps + tombstones, plus a "needs review" conflict queue when two devices changed the same fieldFull 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 outbox row in the same local transaction; a single "sync worker" drains the outbox FIFO per form (ordering matters for edits of the same submission), marks sent_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 sends Idempotency-Key on every POST/PATCH so replays after a dropped response are no-ops. Include a deviceId, clientCreatedAt (device clock) and hlc (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 on online/visibilitychange/app-foreground events; honor Retry-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; a replacement full-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 over BroadcastChannel [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-sqlite useSQLCipher: true (not in Expo Go) [16] or op-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; choose WHEN_UNLOCKED_THIS_DEVICE_ONLY so 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-extractable CryptoKey (extractable: false blocks exportKey/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

OptionLicenceCostBlob handlingMulti-tabSafari privateBundleVerdict
Dexie 4.4.1Apache-2.0free (Cloud optional)Blobs OK, don't index [3]; Cloud offloads >= 4 KB [1]Dexie liveQuery/BroadcastChannelworks (in-memory)smallDefault adapter
RxDB (Dexie storage)Apache-2.0 coreIndexedDB/OPFS/SQLite/crypto plugins $99+/mo annual [5]via attachments pluginleader election built-inworkslargerOptional, customer-licensed
PouchDB 9.0.0Apache-2.0freeattachments in doc modelyesworks~ (unverified)Only for CouchDB shops
wa-sqlite / sqlite-wasm + OPFSMIT / public domainfreefiles in OPFSWeb Locks required [10]no OPFS -> IDB VFS fallback [10]~0.9 MB [7]Optional adapter for large datasets

Implications & recommendations for Rasd Forms

  1. 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 RasdStorageAdapter interface. Do not depend on RxDB premium, Realm or WatermelonDB.
  2. 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.
  3. 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.
  4. 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.
  5. 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 when estimate() shows < 100 MB headroom or when running in private mode; detect and message quota errors as "cannot save offline" rather than crashing.
  6. 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.
  7. 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.
  8. 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 a replacement full-resync escape hatch.
  9. 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.
  10. 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

  1. 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)
  2. Dexie Cloud pricing — https://dexie.org/pricing (accessed 2026-08-15)
  3. 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)
  4. 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)
  5. RxDB premium / pricing — https://rxdb.info/premium/ (accessed 2026-08-15)
  6. RxDB RxStorage overview — https://rxdb.info/rx-storage.html (accessed 2026-08-15)
  7. 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)
  8. RxDB: IndexedDB max storage size limit — https://rxdb.info/articles/indexeddb-max-storage-limit.html (accessed 2026-08-15)
  9. 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)
  10. 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)
  11. 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)
  12. 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)
  13. web.dev: Persistent storage — https://web.dev/articles/persistent-storage (accessed 2026-08-15)
  14. Firefox 115 release notes (IndexedDB in Private Browsing) — https://www.firefox.com/en-US/firefox/115.0/releasenotes/ (accessed 2026-08-15)
  15. caniuse: Background Sync API — https://caniuse.com/background-sync (accessed 2026-08-15)
  16. Expo SQLite docs (SDK 57) — https://docs.expo.dev/versions/latest/sdk/sqlite/ (accessed 2026-08-15)
  17. Expo SecureStore docs (SDK 57) — https://docs.expo.dev/versions/latest/sdk/securestore/ (accessed 2026-08-15)
  18. Expo BackgroundTask docs (SDK 57) — https://docs.expo.dev/versions/latest/sdk/background-task/ (accessed 2026-08-15)
  19. Expo blog: Goodbye background-fetch, hello expo-background-task — https://expo.dev/blog/goodbye-background-fetch-hello-expo-background-task (accessed 2026-08-15)
  20. op-sqlite README — https://github.com/OP-Engineering/op-sqlite (accessed 2026-08-15)
  21. react-native-nitro-sqlite README — https://github.com/margelo/react-native-nitro-sqlite (accessed 2026-08-15)
  22. react-native-quick-sqlite (DEPRECATED) — https://github.com/margelo/react-native-quick-sqlite (accessed 2026-08-15)
  23. PowerSync: React Native database performance comparison (2024-07) — https://powersync.com/blog/react-native-database-performance-comparison (accessed 2026-08-15)
  24. WatermelonDB changelog — https://watermelondb.dev/docs/CHANGELOG (accessed 2026-08-15)
  25. react-native-mmkv releases (v4.3.2, 2026-06-22) — https://github.com/mrousavy/react-native-mmkv/releases (accessed 2026-08-15)
  26. 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)
  27. realm-js discussion #6884: Device Sync deprecation — https://github.com/realm/realm-js/discussions/6884 (accessed 2026-08-15)
  28. Couchbase blog: MongoDB ends mobile support (2025-09-30) — https://www.couchbase.com/blog/realm-mongodb-eol-day-2025/ (accessed 2026-08-15)
  29. PowerSync pricing — https://www.powersync.com/pricing (accessed 2026-08-15)
  30. Electric Cloud pricing — https://electric.ax/pricing (accessed 2026-08-15)
  31. Electric 1.0 released (2025-03-17) — https://electric-sql.com/blog/2025/03/17/electricsql-1.0-released (accessed 2026-08-15)
  32. Zero docs: Offline — https://zero.rocicorp.dev/docs/offline (accessed 2026-08-15)
  33. Turso: Offline Sync public beta (2025-03-31) — https://turso.tech/blog/turso-offline-sync-public-beta (accessed 2026-08-15)
  34. Turso: React Native bindings (2026-01-29) — https://turso.tech/blog/react-native-bindings-for-turso (accessed 2026-08-15)
  35. tus resumable upload protocol 1.0.0 — https://tus.io/protocols/resumable-upload (accessed 2026-08-15)
  36. IETF draft-ietf-httpbis-resumable-upload-12 (2026-07-06) — https://datatracker.ietf.org/doc/draft-ietf-httpbis-resumable-upload/ (accessed 2026-08-15)
  37. tus-js-client — https://github.com/tus/tus-js-client (accessed 2026-08-15)
  38. 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)
  39. crdt-benchmarks (Yjs / Loro / Automerge B4) — https://github.com/zxch3n/crdt-benchmarks (accessed 2026-08-15)
  40. Automerge 3.0 announcement (2025-07) — https://automerge.org/blog/automerge-3/ (accessed 2026-08-15)
  41. loro-crdt on npm — https://www.npmjs.com/package/loro-crdt (accessed 2026-08-15)
  42. Yjs docs: offline support / y-indexeddb — https://docs.yjs.dev/getting-started/allowing-offline-editing (accessed 2026-08-15)
  43. SQLCipher licence (Zetetic) — https://www.zetetic.net/sqlcipher/license/ (accessed 2026-08-15)
  44. MDN: CryptoKey.extractable — https://developer.mozilla.org/en-US/docs/Web/API/CryptoKey/extractable (accessed 2026-08-15)
  45. MDN: Web Locks API — https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API (accessed 2026-08-15)
  46. 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)
  47. Expo SDK 53 changelog — https://expo.dev/changelog/sdk-53 (accessed 2026-08-15)