An Expo app’s database question has two entirely separate answers: on-device storage that lives only on the phone, or a remote database reached through an API you host. Most real apps need both, for different data.
“Expo database” gets asked by people at two different stages: someone who wants to persist state locally on a single device, and someone who wants their app’s data to sync across devices and users. Those are different problems with different tools, and conflating them is how apps end up with database credentials baked into the app bundle — a mistake worth explaining clearly before it happens to you.
Table of contents
- On-device: expo-sqlite, including the newer API
- On-device: AsyncStorage and MMKV
- ORM options on top of SQLite: Drizzle and friends
- Remote: a backend API plus a managed database
- Remote: BaaS as the other option
- Sync, offline-first, and never bundling credentials
- How this fits the rest of the stack
- FAQ
On-device: expo-sqlite, including the newer API
expo-sqlite is the standard on-device relational database for Expo apps. It persists across app restarts, ships as a real SQL engine (not a key-value wrapper), and Expo’s newer API is meaningfully nicer to use than the original callback-based version.
import { useSQLiteContext, SQLiteProvider } from 'expo-sqlite'
function Todos() {
const db = useSQLiteContext()
const [rows, setRows] = useState([])
useEffect(() => {
db.getAllAsync('SELECT * FROM todos').then(setRows)
}, [])
return null // render rows
}
export default function App() {
return (
<SQLiteProvider databaseName="app.db">
<Todos />
</SQLiteProvider>
)
}
Reach for expo-sqlite when the data is genuinely relational — multiple related tables, queries that filter and join — and when it needs to survive the app being closed and reopened. It works on iOS and Android natively, and has a web setup path for Expo web builds too.
On-device: AsyncStorage and MMKV
For simpler needs, a full SQL database is overkill. AsyncStorage is Expo/React Native’s basic key-value store — fine for small amounts of data like a theme preference, an auth token, or a flag, but it is asynchronous, unencrypted by default, and noticeably slower once you are reading and writing frequently.
MMKV is a faster alternative key-value store, synchronous, and commonly reached for when AsyncStorage’s performance becomes noticeable — app settings read on every render, or frequent small writes. It requires a native module, so it needs a development build or EAS Build rather than working in Expo Go out of the box.
- AsyncStorage: simplest API, async, fine for small infrequent key-value data
- MMKV: synchronous, fast, needs a native build
- expo-sqlite: real relational data with queries, joins, and structure
ORM options on top of SQLite: Drizzle and friends
Writing raw SQL strings for every query gets error-prone once a schema has more than a few tables. Drizzle ORM has become the common choice for Expo projects using expo-sqlite — it is lightweight, generates migrations, and gives you typed queries against your schema instead of hand-written SQL strings.
import { drizzle } from 'drizzle-orm/expo-sqlite'
import { openDatabaseSync } from 'expo-sqlite'
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
const todos = sqliteTable('todos', {
id: integer('id').primaryKey(),
title: text('title').notNull(),
done: integer('done', { mode: 'boolean' }).default(false),
})
const db = drizzle(openDatabaseSync('app.db'))
const rows = await db.select().from(todos)
This is worth adopting the moment your schema has relationships worth modeling explicitly, since the type safety catches mismatches between your queries and your schema at compile time rather than at runtime on a user’s device.
Migrations are the part teams underestimate. Drizzle’s migration files let you version your schema changes the same way you version application code, which matters more on mobile than on a server: you cannot SSH into a user’s phone to fix a broken table, so a migration that runs safely on first launch after an app update is not optional, it is the only chance you get.
Remote: a backend API plus a managed database
The moment data needs to exist outside one phone — user accounts, anything two users share, anything that should survive an app reinstall — on-device storage is the wrong tool by itself. The correct shape is an API your app calls over HTTPS, with a real database behind that API, not behind the app.
This is also the point where RunxBuild fits directly: host the backend service and a managed Postgres (or MySQL) instance together, with connection limits, backups, and private networking between the API and the database already handled. The Expo app talks to your API over the network; it never talks to the database directly.
See Databases on RunxBuild for how the managed instance side of this works.
Remote: BaaS as the other option
A backend-as-a-service platform is the alternative to hosting your own API when you want managed auth, realtime subscriptions, and a database behind a generated client SDK without writing the API layer yourself. It trades some control and a monthly bill for less backend code.
The right call depends on how custom your backend logic needs to be. A straightforward CRUD app with standard auth fits a BaaS well. An app with non-trivial business logic, background jobs, or integrations usually outgrows a BaaS’s generated API faster than it outgrows a hand-written one, and at that point self-hosting the API becomes the simpler long-term path, not the more complex one.
Sync, offline-first, and never bundling credentials
Most real apps use both layers together: expo-sqlite (or AsyncStorage/MMKV) as a local cache that makes the app usable offline and instantly responsive, syncing to the remote database through your API when connectivity allows. This offline-first pattern is why the two “expo database” answers are not actually competing — they solve different halves of the same problem.
The one rule that cannot bend: never put database credentials, connection strings, or a database host address inside the app bundle. Anything shipped in a mobile app binary can be extracted by anyone who downloads it, regardless of obfuscation. The app should only ever hold an API base URL and, after login, a scoped auth token — the database itself should be reachable only from your backend, on a private network.
This is not a theoretical risk. Mobile app binaries are trivially decompiled, and a hardcoded database connection string found that way hands an attacker direct read-write access to every user’s data, bypassing whatever authentication logic your app’s UI enforces. An API layer is not bureaucracy here — it is the actual security boundary, since it is the one place you control that a decompiled binary cannot reach into.
How this fits the rest of the stack
The two halves of this question resolve cleanly once you separate them: expo-sqlite, AsyncStorage, or MMKV for what lives on the device, and a hosted API in front of a real database for anything that needs to survive past one phone. RunxBuild hosts both the backend service and the managed Postgres or MySQL instance behind it, so the app never talks to the database directly. The RunxBuild hosting calculator shows what that API-plus-database pairing actually costs before you commit to the architecture.
The API and the managed Postgres it talks to can both be created from the RunxBuild dashboard, which keeps the credentials on the server where they belong.
Useful related references:
- Make Your Own Mobile App
- SQLite vs PostgreSQL: Which One Your Project Actually Needs
- Prisma with Postgres
- Databases on RunxBuild
FAQ
Does Expo have a built-in database?
Expo ships expo-sqlite as an official module for on-device relational storage, plus AsyncStorage for simple key-value data. Neither is a remote database — both live entirely on the device and do not sync anywhere on their own.
Can I use SQLite with Expo?
Yes, via the expo-sqlite package, which works in a standard Expo managed workflow. Its newer API supports async queries, migrations, and works with ORMs like Drizzle for typed schema access.
What is the difference between AsyncStorage and SQLite in Expo?
AsyncStorage is a simple asynchronous key-value store best for small settings-style data. expo-sqlite is a full relational database supporting real SQL queries, joins, and structured schemas, appropriate once your data has meaningful relationships.
Can I connect an Expo app directly to PostgreSQL?
You should not connect directly from the app. Postgres expects a persistent authenticated connection and credentials that a mobile app bundle cannot keep secret. The correct pattern is an API layer between the app and the database, with the app calling the API over HTTPS.
How do I sync a local Expo database with a server?
Build an API endpoint that accepts and returns the data your local database stores, then write sync logic in the app that pushes local changes when online and pulls remote changes into the local database. There is no built-in Expo sync feature — this logic is application-specific, though libraries exist to help with conflict resolution for offline-first apps.