This comparison evaluates five prominent data storage solutions for JavaScript environments. expo-sqlite provides a managed SQLite interface for Expo applications. pouchdb offers a NoSQL JSON database that syncs with CouchDB. react-native-sqlite-storage is a legacy SQLite binding for React Native. realm is an object database with optional MongoDB synchronization. sqlite3 is a native Node.js binding for SQLite servers and Electron apps.
Choosing the right storage layer is critical for JavaScript applications that need to work offline or handle large datasets locally. The packages expo-sqlite, pouchdb, react-native-sqlite-storage, realm, and sqlite3 all solve this problem but target different environments and data models. Let's break down how they differ in practice.
The first decision is where your code runs. Some packages are built for mobile, others for servers, and some for both.
expo-sqlite is designed specifically for the Expo managed workflow.
// expo-sqlite: Opening a database
import { useExpoSQLiteDatabase } from 'expo-sqlite';
const db = useExpoSQLiteDatabase('my.db');
pouchdb runs in browsers, Node.js, and React Native.
// pouchdb: Creating a database instance
import PouchDB from 'pouchdb';
const db = new PouchDB('my_database');
react-native-sqlite-storage targets bare React Native projects.
// react-native-sqlite-storage: Opening a database
import SQLite from 'react-native-sqlite-storage';
const db = SQLite.openDatabase({ name: 'my.db' });
realm works in React Native, Node.js, and Electron.
// realm: Opening a Realm
import Realm from 'realm';
const realm = new Realm({ schema: [PersonSchema] });
sqlite3 is strictly for Node.js and Electron.
// sqlite3: Opening a database in Node
import sqlite3 from 'sqlite3';
const db = new sqlite3.Database('my.db');
How you structure data changes significantly between these tools. Some use tables, others use documents or objects.
expo-sqlite uses standard relational tables.
// expo-sqlite: Creating a table
await db.execAsync(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY NOT NULL,
name TEXT
);
`);
pouchdb stores JSON documents.
_id.// pouchdb: Saving a document
await db.put({
_id: 'user1',
name: 'Alice',
role: 'admin'
});
react-native-sqlite-storage also uses relational tables.
expo-sqlite but different API.// react-native-sqlite-storage: Creating a table
db.transaction(tx => {
tx.executeSql(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY NOT NULL,
name TEXT
);
`);
});
realm uses live objects based on schemas.
// realm: Defining a schema
const PersonSchema = {
name: 'Person',
properties: {
name: 'string',
age: 'int'
}
};
sqlite3 uses standard relational tables.
expo-sqlite.// sqlite3: Creating a table
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT
)
`);
Reading data is where you spend most of your time. The experience ranges from raw SQL to object queries.
expo-sqlite uses async/await with SQL strings.
// expo-sqlite: Querying rows
const result = await db.getFirstAsync('SELECT * FROM users WHERE id = ?', [1]);
pouchdb uses map-reduce or Mango queries.
// pouchdb: Finding documents
const result = await db.find({
selector: { name: 'Alice' }
});
react-native-sqlite-storage uses callback-based transactions.
// react-native-sqlite-storage: Querying rows
db.transaction(tx => {
tx.executeSql('SELECT * FROM users', [], (tx, results) => {
console.log(results.rows.item(0));
});
});
realm uses object queries with filters.
// realm: Querying objects
const people = realm.objects('Person').filtered('age > 18');
sqlite3 uses callbacks or promises via wrappers.
better-sqlite3 for sync usage.// sqlite3: Querying rows
db.all('SELECT * FROM users', [], (err, rows) => {
console.log(rows);
});
Long-term support matters when your app grows. Some packages are stable, while others are fading.
expo-sqlite is actively maintained by the Expo team.
pouchdb has a long history and stable community.
react-native-sqlite-storage is effectively legacy.
realm is backed by MongoDB.
sqlite3 is the standard for Node.js.
| Feature | expo-sqlite | pouchdb | react-native-sqlite-storage | realm | sqlite3 |
|---|---|---|---|---|---|
| Primary Env | Expo Mobile | Web / Mobile / Node | Bare React Native | Mobile / Node | Node / Electron |
| Data Model | SQL Tables | JSON Documents | SQL Tables | Objects | SQL Tables |
| Sync | Manual | Built-in CouchDB | Manual | MongoDB Atlas | Manual |
| API Style | Async/Await | Promise | Callback | Live Objects | Callback |
| Status | ✅ Active | ✅ Active | ❌ Legacy | ✅ Active | ✅ Active |
expo-sqlite is the default choice for Expo developers. It removes native complexity while giving you full SQL power. Stick with this if you are in the managed workflow.
pouchdb shines when synchronization is the main goal. If you need to sync local data with a remote CouchDB instance without writing custom API endpoints, this is the tool.
react-native-sqlite-storage should be avoided in new work. It lacks the modern ergonomics and stability of expo-sqlite or realm. Migrating away from it is common in mature codebases.
realm is best for complex object graphs. If your data looks more like connected objects than rows in a table, Realm reduces boilerplate. The optional cloud sync is a major bonus for some teams.
sqlite3 belongs on the server or desktop. Use it for Node.js CLI tools or Electron apps. Do not try to force it into a mobile React Native build unless you have a very specific native module setup.
Final Thought: Match the database to your environment first. If you are on Expo, use expo-sqlite. If you need sync, look at pouchdb or realm. If you are on Node.js server, sqlite3 is the standard. Avoid legacy packages unless maintaining old code.
Choose sqlite3 if you are building a Node.js command-line tool or an Electron desktop app. It does not work in standard React Native or browser environments. It requires native compilation via node-gyp. It is the standard for server-side SQLite usage.
Choose expo-sqlite if you are building an app with the Expo managed workflow. It offers the best stability and ease of setup within that ecosystem. You get standard SQL power without handling native code. It is the safest bet for new Expo projects.
Choose pouchdb if your app needs to sync data with a CouchDB server or requires a NoSQL JSON document store. It works well in browsers and React Native via adapters. It is ideal for offline-first applications that need bidirectional sync.
Choose realm if you prefer an object-oriented database over SQL tables. It is a strong choice if you plan to use MongoDB Atlas Device Sync. It handles complex relationships well without writing SQL queries. Performance is generally high for large datasets.
Avoid react-native-sqlite-storage for new projects. It is considered legacy and often breaks with newer React Native versions. Maintenance is sporadic compared to modern alternatives. Only use it if you are maintaining an old app that already depends on it.
Note: This repository is currently unmaintained. We will not update any of its issues or pull requests.
Asynchronous, non-blocking SQLite3 bindings for Node.js.
You can use npm or yarn to install sqlite3:
npm install sqlite3
# or
yarn add sqlite3
master branch: npm install https://github.com/tryghost/node-sqlite3/tarball/mastersqlite3 v5+ was rewritten to use Node-API so prebuilt binaries do not need to be built for specific Node versions. sqlite3 currently builds for both Node-API v3 and v6. Check the Node-API version matrix to ensure your Node version supports one of these. The prebuilt binaries should be supported on Node v20.17.0+.
The module uses prebuild-install to download the prebuilt binary for your platform, if it exists. These binaries are hosted on GitHub Releases for sqlite3 versions above 5.0.2, and they are hosted on S3 otherwise. The following targets are currently provided:
darwin-arm64darwin-x64linux-arm64linux-x64linuxmusl-arm64linuxmusl-x64win32-x64Unfortunately, prebuild cannot differentiate between armv6 and armv7, and instead uses arm as the {arch}. Until that is fixed, you will still need to install sqlite3 from source.
Support for other platforms and architectures may be added in the future if CI supports building on them.
If your environment isn't supported, it'll use node-gyp to build SQLite, but you will need to install a C++ compiler and linker.
It is also possible to make your own build of sqlite3 from its source instead of its npm package (See below.).
The sqlite3 module also works with node-webkit if node-webkit contains a supported version of Node.js engine. (See below.)
SQLite's SQLCipher extension is also supported. (See below.)
See the API documentation in the wiki.
Note: the module must be installed before use.
const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database(':memory:');
db.serialize(() => {
db.run("CREATE TABLE lorem (info TEXT)");
const stmt = db.prepare("INSERT INTO lorem VALUES (?)");
for (let i = 0; i < 10; i++) {
stmt.run("Ipsum " + i);
}
stmt.finalize();
db.each("SELECT rowid AS id, info FROM lorem", (err, row) => {
console.log(row.id + ": " + row.info);
});
});
db.close();
To skip searching for pre-compiled binaries, and force a build from source, use
npm install --build-from-source
The sqlite3 module depends only on libsqlite3. However, by default, an internal/bundled copy of sqlite will be built and statically linked, so an externally installed sqlite3 is not required.
If you wish to install against an external sqlite then you need to pass the --sqlite argument to npm wrapper:
npm install --build-from-source --sqlite=/usr/local
If building against an external sqlite3 make sure to have the development headers available. Mac OS X ships with these by default. If you don't have them installed, install the -dev package with your package manager, e.g. apt-get install libsqlite3-dev for Debian/Ubuntu. Make sure that you have at least libsqlite3 >= 3.6.
Note, if building against homebrew-installed sqlite on OS X you can do:
npm install --build-from-source --sqlite=/usr/local/opt/sqlite/
The default sqlite file header is "SQLite format 3". You can specify a different magic, though this will make standard tools and libraries unable to work with your files.
npm install --build-from-source --sqlite_magic="MyCustomMagic15"
Note that the magic must be exactly 15 characters long (16 bytes including null terminator).
Because of ABI differences, sqlite3 must be built in a custom to be used with node-webkit.
To build sqlite3 for node-webkit:
Install nw-gyp globally: npm install nw-gyp -g (unless already installed)
Build the module with the custom flags of --runtime, --target_arch, and --target:
NODE_WEBKIT_VERSION="0.8.6" # see latest version at https://github.com/rogerwang/node-webkit#downloads
npm install sqlite3 --build-from-source --runtime=node-webkit --target_arch=ia32 --target=$(NODE_WEBKIT_VERSION)
You can also run this command from within a sqlite3 checkout:
npm install --build-from-source --runtime=node-webkit --target_arch=ia32 --target=$(NODE_WEBKIT_VERSION)
Remember the following:
You must provide the right --target_arch flag. ia32 is needed to target 32bit node-webkit builds, while x64 will target 64bit node-webkit builds (if available for your platform).
After the sqlite3 package is built for node-webkit it cannot run in the vanilla Node.js (and vice versa).
npm test of the node-webkit's package would fail.Visit the “Using Node modules” article in the node-webkit's wiki for more details.
For instructions on building SQLCipher, see Building SQLCipher for Node.js. Alternatively, you can install it with your local package manager.
To run against SQLCipher, you need to compile sqlite3 from source by passing build options like:
npm install sqlite3 --build-from-source --sqlite_libname=sqlcipher --sqlite=/usr/
node -e 'require("sqlite3")'
If your SQLCipher is installed in a custom location (if you compiled and installed it yourself), you'll need to set some environment variables:
Set the location where brew installed it:
export LDFLAGS="-L`brew --prefix`/opt/sqlcipher/lib"
export CPPFLAGS="-I`brew --prefix`/opt/sqlcipher/include/sqlcipher"
npm install sqlite3 --build-from-source --sqlite_libname=sqlcipher --sqlite=`brew --prefix`
node -e 'require("sqlite3")'
Set the location where make installed it:
export LDFLAGS="-L/usr/local/lib"
export CPPFLAGS="-I/usr/local/include -I/usr/local/include/sqlcipher"
export CXXFLAGS="$CPPFLAGS"
npm install sqlite3 --build-from-source --sqlite_libname=sqlcipher --sqlite=/usr/local --verbose
node -e 'require("sqlite3")'
Running sqlite3 through electron-rebuild does not preserve the SQLCipher extension, so some additional flags are needed to make this build Electron compatible. Your npm install sqlite3 --build-from-source command needs these additional flags (be sure to replace the target version with the current Electron version you are working with):
--runtime=electron --target=18.2.1 --dist-url=https://electronjs.org/headers
In the case of MacOS with Homebrew, the command should look like the following:
npm install sqlite3 --build-from-source --sqlite_libname=sqlcipher --sqlite=`brew --prefix` --runtime=electron --target=18.2.1 --dist-url=https://electronjs.org/headers
npm test
Thanks to Orlando Vazquez, Eric Fredricksen and Ryan Dahl for their SQLite bindings for node, and to mraleph on Freenode's #v8 for answering questions.
This module was originally created by Mapbox & is now maintained by Ghost.
We use GitHub releases for notes on the latest versions. See CHANGELOG.md in git history for details on older versions.
Copyright (c) 2013-2026 Mapbox & Ghost Foundation
node-sqlite3 is BSD licensed.