All articles
Mobile DevelopmentSeptember 7, 2026 6 min read

Offline-First React Native in 2026: Choosing Between WatermelonDB, PowerSync, and Plain SQLite

We've shipped offline-first React Native apps on WatermelonDB, PowerSync, and hand-rolled SQLite. Here's how to pick the right one before you've written 40k lines of sync code you'll regret.

Offline-First React Native in 2026: Choosing Between WatermelonDB, PowerSync, and Plain SQLite

Every offline-first pitch starts the same way: "We just need to cache a few things locally." Six months later there's a queue of pending writes, a conflict-resolution ticket nobody wants, and a support thread titled "my edits disappeared." Picking the right local store on day one is the cheapest decision you'll ever make.

We've shipped React Native apps on all three of the common paths in 2026 — WatermelonDB, PowerSync, and raw SQLite via op-sqlite or expo-sqlite. Here's how they actually behave once you have real users, real network conditions, and a backend team that keeps changing the schema.

What "offline-first" actually means in 2026

Before comparing tools, agree on what you're building. There are three flavors people conflate:

  1. Read-only cache — you fetch data, store it locally so the app opens fast, and re-sync when online. No writes to reconcile.
  2. Optimistic write queue — users can create/edit while offline; changes get flushed to the server later. Conflicts are rare because each user mostly owns their own data.
  3. True multi-writer sync — multiple devices edit the same records offline, and the system must merge them. This is where naive approaches die.

Most apps are category 2. If you're actually in category 3 (collaborative editors, shared inventories, field-service apps with multiple techs on the same job), your tooling choice matters a lot more.

The React Native constraint that shapes everything

SQLite is the only serious local store on mobile. IndexedDB shims exist, MMKV is great for key-value, but for anything relational you're on SQLite. The question is what sits on top of it — a sync engine, an ORM, or nothing.

Option 1: Raw SQLite with op-sqlite or expo-sqlite

The minimal path. You pick a driver — op-sqlite if you want the fastest JSI-based option, expo-sqlite if you're staying inside the Expo managed workflow — and you write your own persistence and sync code.

import { open } from '@op-engineering/op-sqlite';

const db = open({ name: 'app.db' });

db.execute(`
  CREATE TABLE IF NOT EXISTS jobs (
    id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    updated_at INTEGER NOT NULL,
    dirty INTEGER DEFAULT 0
  );
`);

async function saveJob(job) {
  await db.execute(
    `INSERT OR REPLACE INTO jobs (id, title, updated_at, dirty)
     VALUES (?, ?, ?, 1)`,
    [job.id, job.title, Date.now()]
  );
}

The good: zero abstraction tax, you understand every byte, upgrades are trivial. Query performance is excellent — op-sqlite with JSI easily handles tens of thousands of rows without the bridge choking.

The bad: you are now on the hook for change tracking, tombstones, retry policy, exponential backoff, conflict resolution, schema migrations, and reactive query invalidation. Every one of those is a solved problem that you're re-solving.

Pick this when: you're in category 1 (read-only cache) or a very simple category 2 with server-authoritative data. Also when your team has strong SQL skills and doesn't want another dependency to babysit through React Native upgrades.

Option 2: WatermelonDB

WatermelonDB has been the default "real" offline-first ORM for React Native for years, and in 2026 it still holds up. It's a reactive layer on top of SQLite with lazy loading, observables that plug into React, and a sync protocol you implement against your backend.

The model is opinionated: you define collections, records are observed, and queries return observables that re-render components when data changes. You bring your own sync endpoints, but the framework gives you the synchronize() function that handles the pull/push dance and dirty tracking.

Where WatermelonDB shines

  • Reactivity is free. Subscribe to a query, and the UI updates when any relevant row changes. No manual invalidation.
  • Lazy loading works. Lists of 50k records don't melt the JS thread because records are only materialized when observed.
  • Sync is your protocol, not theirs. You control the wire format, auth, and server. No vendor lock-in on the backend.

Where it hurts

  • Conflict resolution is on you. The sync protocol assumes last-write-wins unless you build merge logic server-side. For category 3 apps, you'll write real code.
  • The New Architecture migration was bumpy. Most issues are resolved now, but if you're on an older version, budget time.
  • Schema migrations require care. You must write migration steps for every schema change, and getting them wrong bricks the app on upgrade.

Pick this when: you're a category 2 app with a backend team that can build the sync endpoints, and you want reactive queries without writing them yourself.

Option 3: PowerSync (and the Postgres-sync category)

PowerSync is the most interesting entrant of the last couple of years. It's a sync engine that connects a local SQLite database on-device to a Postgres database on the server, using a change-streaming protocol. You write regular SQL on both ends and PowerSync handles the delta.

There are similar products in this space now — the pattern is "local SQLite that mirrors a filtered slice of your production database." You define sync rules (which rows each user gets), and the engine keeps them in sync.

What you get

  • Sync is not your problem anymore. Writes are queued, retried, deduped, and applied. Conflict resolution is bucket-based and mostly reasonable defaults.
  • You write SQL on both ends. No custom ORM, no bespoke protocol. Your Postgres schema is the source of truth.
  • Row-level sync rules map cleanly to multi-tenant apps — each user only downloads the data they can see.

What you're signing up for

  • A vendor and a service. Even self-hosted, it's infrastructure to run. Pricing at scale is a real conversation.
  • Postgres bias. If your backend is Mongo, DynamoDB, or a services-behind-REST setup, the fit is worse.
  • Less UI reactivity out of the box than WatermelonDB — you'll typically pair it with a query library like TanStack Query or wrap it yourself.

Pick this when: you have a Postgres-backed backend, you're doing category 2 or 3 sync at scale, and you'd rather pay a vendor than staff a sync team.

A decision framework that actually works

Skip the feature matrix. Answer these four questions in order:

  1. Do users write data offline that other users will see? If no → raw SQLite is probably enough.
  2. Is your backend Postgres (or willing to be)? If yes and you answered yes to #1 → seriously evaluate PowerSync.
  3. Do you need reactive queries driving your UI? If yes and you're not on PowerSync → WatermelonDB.
  4. Is your team allergic to vendor dependencies or SDK-heavy stacks? If yes → raw SQLite with a hand-rolled sync layer, and accept the maintenance cost.

The migration trap

Whatever you pick, assume you'll want to change it in year three. The mitigation is to keep your domain models separate from your storage layer. Repository pattern, a thin data-access module, whatever you call it — don't sprinkle database.get('jobs').query(...) across 200 components. We've done that migration and it's grim.

OTA updates and the schema problem

One thing people forget: if you're shipping schema changes via EAS Update or CodePush, you can push JS that expects a new column before the migration has run. Guard your queries, version your schema in the JS layer, and refuse to run new business logic until the migration completes. Otherwise you'll ship an OTA on Friday and spend Saturday reading crash reports.

For the deeper OTA mechanics, we wrote about what OTA actually solves on the 72Technologies blog.

Where we'd start

If we were kicking off a new React Native app tomorrow with real offline requirements, we'd default to WatermelonDB for category 2 apps on custom backends, and PowerSync when the backend is Postgres and sync complexity is the main risk. Raw SQLite via op-sqlite is our pick for read-heavy caches and prototypes where we want to defer the sync decision.

Whatever you pick, wrap it. Build a thin repository layer this week, not next year. And write your first schema migration on day one — even if it's a no-op — so the muscle exists before you need it in a hurry.

If you want a second pair of eyes on an offline-first architecture before you commit, our mobile team does exactly this kind of review.

#React Native#Expo#Offline First#Databases#Mobile Architecture

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project