Summary
WatermelonDB in React Native is best understood as an offline-first data layer for apps that need fast local queries, reactive UI updates, and server synchronization on top of SQLite. It is not just SQLite with a nicer API: it adds schema/model rules, query APIs, writer transactions, observables, and sync primitives. For a few settings or a small cache it is probably too much, but for thousands or tens of thousands of records that must keep working offline, it can be more productive than hand-writing SQLite plumbing.
Need the SQLite basics first? WAL mode, file-based database behavior, and lock-error troubleshooting are covered separately in the SQLite practical guide ↗.
Version check: at the time this Korean source was written, npm showed
@nozbe/watermelondb0.28.0 while some official documentation pages still displayed 0.27.1. Before adopting it, check your React Native/Expo version, the WatermelonDB release notes, and open GitHub issues together.
In this article
- Why this matters
- What WatermelonDB is
- Why it can stay fast with large local data
- Basic React Native setup
- SQLite vs. WatermelonDB
- How synchronization works
- Where teams usually get stuck
- When to use it, and when to avoid it
- Conclusion
- References
Why this matters
A React Native app often starts by calling an API and rendering the response. That works until users must create or edit data offline, the app must show thousands of tasks or messages immediately after launch, local changes must refresh several screens automatically, or server synchronization becomes a requirement. SQLite is a strong local database choice, but direct SQLite use means you design SQL access, result mapping, state refresh, migrations, conflicts, and sync logic yourself. WatermelonDB puts a React/React Native-oriented data framework on top of SQLite.
What WatermelonDB is
WatermelonDB is an open-source local database framework by Nozbe. Its core idea is to keep large local datasets usable in React and React Native without loading everything into JavaScript memory at startup.
- It reads data lazily instead of loading the entire database into JS memory.
- Queries run in a native database such as SQLite.
- RxJS-based observables can update UI when data changes.
- It provides model, collection, query, writer, migration, and sync APIs.
- For offline-first sync, you still need your own backend.
| Component | Role |
|---|---|
| Schema | Defines tables and columns. |
| Model | Defines the objects used in app code. |
| Collection | Access point for records in a table. |
| Query API | Runs condition-based queries. |
| Writer | Groups create, update, and delete operations into safe write blocks. |
| Observable | Lets UI react when records or queries change. |
| Sync API | Defines how local and server-side changes are exchanged. |
Why it can stay fast with large local data
The important point is not only that SQLite is fast. The bigger point is that WatermelonDB tries not to pull unnecessary data across the native-to-JavaScript boundary. It queries only what a screen needs, keeps filtering in the database instead of in JS arrays, and updates only the components connected to changed observable data.
Basic React Native setup
1. Install the packages
npm install @nozbe/watermelondb
npm install -D @babel/plugin-proposal-decorators
yarn add @nozbe/watermelondb
yarn add --dev @babel/plugin-proposal-decorators
WatermelonDB examples use decorators, so Metro/Babel must be configured for legacy decorators.
{
"presets": ["module:metro-react-native-babel-preset"],
"plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }]]
}
On iOS, CocoaPods setup may also be required.
# ios/Podfile
pod 'simdjson', path: '../node_modules/@nozbe/simdjson', modular_headers: true
cd ios
pod install
Projects using use_frameworks!, Expo, or React Native New Architecture should verify iOS and Android builds in a small branch before making WatermelonDB part of the main code path.
2. Define schema and models
// src/db/schema.js
import { appSchema, tableSchema } from '@nozbe/watermelondb'
export const mySchema = appSchema({
version: 1,
tables: [
tableSchema({ name: 'posts', columns: [
{ name: 'title', type: 'string' },
{ name: 'body', type: 'string' },
{ name: 'is_pinned', type: 'boolean' },
]}),
tableSchema({ name: 'comments', columns: [
{ name: 'body', type: 'string' },
{ name: 'post_id', type: 'string', isIndexed: true },
]}),
],
})
Columns such as post_id that are used frequently for relationship lookups should be indexed. Writes should happen inside database.write() or @writer; otherwise the data layer becomes difficult to reason about.
// src/db/index.js
import { Database } from '@nozbe/watermelondb'
import SQLiteAdapter from '@nozbe/watermelondb/adapters/sqlite'
const adapter = new SQLiteAdapter({ schema: mySchema })
export const database = new Database({ adapter, modelClasses: [Post, Comment] })
import { Q } from '@nozbe/watermelondb'
const pinnedPosts = await database
.get('posts')
.query(Q.where('is_pinned', true))
.fetch()
3. Connect data to React UI
import { withObservables } from '@nozbe/watermelondb/react'
function PostItem({ post }) {
return <Text>{post.title}</Text>
}
export default withObservables(['post'], ({ post }) => ({ post }))(PostItem)
This is the part that makes WatermelonDB different from a plain local-storage wrapper: a record or query can be observed, and connected UI can update when the underlying data changes.
SQLite vs. WatermelonDB
The practical question is not which one is universally better. It is how much control you need, and how much application-layer structure you want WatermelonDB to provide.
| Area | Using SQLite directly | Using WatermelonDB |
|---|---|---|
| Base role | Local relational database engine/API | SQLite-based reactive data framework |
| Data access | You write SQL and map results yourself | You use models, collections, and query APIs |
| Performance | Excellent when queries are designed well | Lazy loading, native queries, and observable updates |
| React UI | You manage state and refresh logic | Observables can refresh connected UI |
| Sync | You implement everything | Sync primitives are provided, but the backend is still yours |
Use SQLite directly when
- The local dataset is small.
- You only need settings, cache, or recent search history.
- SQL control and tuning matter more than model abstraction.
- You do not need automatic React UI updates from local data changes.
For React Native, alternatives such as expo-sqlite, react-native-sqlite-storage, op-sqlite, and react-native-quick-sqlite may be simpler, especially in Expo projects.
Consider WatermelonDB when
- You are building an offline-first app.
- The local database may grow to thousands or tens of thousands of rows.
- Data must be created and edited while offline.
- Multiple screens need to react to the same local data.
- You can implement and maintain the backend sync protocol.
- The team can handle React Native native build issues.
How synchronization works
WatermelonDB includes sync primitives, but it does not build the server for you. The client side calls synchronize() and provides pullChanges and pushChanges. The backend must return a consistent changes object and timestamp.
import { synchronize } from '@nozbe/watermelondb/sync'
export async function syncDatabase(database) {
await synchronize({
database,
pullChanges: async ({ lastPulledAt, schemaVersion }) => {
const response = await fetch(`https://api.example.com/sync?last_pulled_at=${lastPulledAt ?? ''}&schema_version=${schemaVersion}`)
if (!response.ok) throw new Error(await response.text())
const { changes, timestamp } = await response.json()
return { changes, timestamp }
},
pushChanges: async ({ changes, lastPulledAt }) => {
const response = await fetch(`https://api.example.com/sync?last_pulled_at=${lastPulledAt ?? ''}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(changes) })
if (!response.ok) throw new Error(await response.text())
},
migrationsEnabledAtVersion: 1,
})
}
The hard part is making sure the backend never misses changes after lastPulledAt, handles deleted IDs, uses a consistent server timestamp, and has a clear conflict policy.
Where teams usually get stuck
Case 1. Decorator syntax is not recognized
SyntaxError: Support for the experimental syntax 'decorators' isn't currently enabled
Check that the dependency and Metro/Babel config are both present. A common mistake is to look only at TypeScript settings.
Case 2. iOS build fails around simdjson.h or Pods
Check ios/Podfile, run pod install, and compare your setup with the official installation guide. If the project uses use_frameworks!, verify carefully.
Case 3. Expo Go cannot find the native module
NativeModules.WMDatabaseBridge is not defined
Expo Go does not include arbitrary native modules. Use an Expo development build, review config/native setup, or reduce scope to expo-sqlite if the requirements are simple.
Case 4. New Architecture compatibility is uncertain
For React Native 0.7x+, Bridgeless, and newer Expo SDK combinations, check WatermelonDB issues before adoption.
npm view @nozbe/watermelondb version
npm view react-native version
Case 5. Sync duplicates or misses records
If sync is the core requirement, backend protocol design matters more than simply installing WatermelonDB. Check server timestamp generation, possible missing changes during pull, deleted ID handling, and conflict policy.
When to use it, and when to avoid it
Good fit
- Offline-first app behavior is a real requirement.
- The local dataset is large and startup speed matters.
- Relational data and reactive screens are important.
- You can build a reliable sync backend.
- Your team can debug React Native native modules.
Hold off
- You only store settings or a short-lived API cache.
- You must stay inside Expo Go without native build management.
- You are already adopting a new RN architecture and cannot spend time verifying compatibility.
- You need complex SQL analytics more than a model-based app data layer.
- You cannot design or operate the sync backend.
Conclusion
WatermelonDB is more than a convenience wrapper around SQLite. It is a data framework for React Native apps that combine large local datasets, reactive UI, and offline-first synchronization. If most of those needs are real, test WatermelonDB in a small branch. If you only need a small cache or a few settings, start with a simpler SQLite library and move later if the data layer grows.
References
- WatermelonDB official documentation
- WatermelonDB Installation
- WatermelonDB Schema
- WatermelonDB Model
- WatermelonDB Querying
- WatermelonDB Sync Backend
- WatermelonDB GitHub
- npm @nozbe/watermelondb
- Expo SQLite documentation
- op-sqlite GitHub
Related Posts
- React Native Nitro and TurboModule explained: how to choose native modules in the New Architecture
- What is SQLite? Advantages, limitations, and practical usage of a file-based database without a server
Please show some love to Korean, too.