Your users don't care about your sync architecture. They care that the app works in a subway tunnel, on a plane, or when the cell signal dies at a conference. Offline-first isn't a feature. It's the baseline for retention.
Why WatermelonDB And SQLite Still Win In 2026
React Native developers have three real choices for local persistence: raw SQLite via expo-sqlite or react-native-quick-sqlite, PowerSync for a managed backend, or WatermelonDB. Raw SQL gives control but demands you write migration logic, conflict resolution, and a reactive query layer yourself. PowerSync removes backend ops but locks you into their cloud and pricing model. WatermelonDB sits in the sweet spot: a lazy, observable layer on top of SQLite that handles schema migrations, conflict-free replicated data types (CRDTs) for merge safety, and a JSI bridge that keeps the JS thread free.
Model Layer And Lazy Queries
Watermelon models map 1:1 to tables. The key difference from ORMs you know: queries are lazy. Calling database.get('posts').query(Q.where('author_id', authorId)) returns a Query object, not data. Data loads only when you subscribe or call fetch(). This keeps startup fast on low-end Android devices because you never hydrate the full table into memory.
Sync Strategy: Pull, Push, Resolve
Watermelon doesn't ship a sync server. You build the contract. The pattern that survives production:
1. Timestamp-based pull. Client sends last_pulled_at. Server returns rows where updated_at > last_pulled_at plus a tombstone list for deletes.
2. Optimistic push. Local writes mark _status: 'pending'. Background worker POSTs changes. On 2xx, mark synced. On 409, fetch server version, merge via CRDT, retry.
3. CRDT merge for text. Use Yjs or Automerge for collaborative fields. For scalar fields, last-writer-wins on updated_at with a client-generated UUID tiebreaker.
MMKV For Ephemeral State
SQLite is heavy for tiny, high-frequency keys: theme preference, onboarding step, last seen tutorial ID. MMKV uses JSI and mmap for sub-millisecond reads. Keep it separate from Watermelon.
Migration Reality Check
Watermelon migrations run on the SQLite thread via JSI. They are fast but irreversible. Test on a device with 2 GB RAM and a 50k-row database. Common failure: adding a non-null column without a default. Always provide default in the migration or backfill in a separate step.
✦
Comparison At A Glance
| Dimension | WatermelonDB | PowerSync | Raw SQLite | ||||
|---|---|---|---|---|---|---|---|
| Setup time | Medium (schema + sync) | Low (managed backend) | High (build everything) | Sync ownership | You | Shared | You |
| CRDT support | Built-in (via plugins) | Native | Manual | ||||
| Bundle size | ~120 KB | ~200 KB (SDK) | ~50 KB | ||||
| Offline query speed | Lazy, indexed | Lazy, indexed | Manual indexes | ||||
| Backend lock-in | None | Yes | None |
Ship The Resilient Version
Start with WatermelonDB schema version 1. Add MMKV for UI state. Write a 200-line sync worker that pulls deltas every 30 seconds and pushes pending writes with exponential backoff. Test by killing the network mid-write. If the app recovers without data loss, you've earned the offline-first label.