When your app needs to work offline with structured data — a todo list, a cached feed, saved records you filter and sort — key/value storage isn't enough. expo-sqlite ships a full SQLite database on the device with a clean async API. It's the most-searched Expo module for offline data, and this guide covers everything from opening a database to transactions.
Install
npx expo install expo-sqlite
Open a database and create a table
import * as SQLite from "expo-sqlite"
const db = await SQLite.openDatabaseAsync("app.db")
await db.execAsync(`
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY NOT NULL,
text TEXT NOT NULL,
done INTEGER DEFAULT 0
);
`)
openDatabaseAsync creates the file if it doesn't exist and reuses it on the next launch.
Insert with parameters (safe from injection)
Use ? placeholders and pass values as arguments — never concatenate strings:
await db.runAsync("INSERT INTO todos (text) VALUES (?)", "Ship the app")
// runAsync returns metadata about the write:
const result = await db.runAsync("INSERT INTO todos (text) VALUES (?)", "Write tests")
console.log(result.lastInsertRowId, result.changes)
Query rows
Pick the helper that matches what you need back:
// All rows
const todos = await db.getAllAsync<{ id: number; text: string; done: number }>(
"SELECT * FROM todos ORDER BY id DESC"
)
// A single row
const first = await db.getFirstAsync("SELECT * FROM todos WHERE id = ?", 1)
// Stream row-by-row for large results
for await (const row of db.getEachAsync("SELECT * FROM todos")) {
console.log(row)
}
Update and delete
await db.runAsync("UPDATE todos SET done = 1 WHERE id = ?", 1)
await db.runAsync("DELETE FROM todos WHERE id = ?", 1)
Use transactions for batched writes
Wrap multiple writes so they all succeed or all roll back — faster and safer:
await db.withTransactionAsync(async () => {
await db.runAsync("INSERT INTO todos (text) VALUES (?)", "Task A")
await db.runAsync("INSERT INTO todos (text) VALUES (?)", "Task B")
await db.runAsync("INSERT INTO todos (text) VALUES (?)", "Task C")
})
Provide the database with a React context
For app-wide access, wrap your tree in SQLiteProvider and read it with useSQLiteContext():
import { SQLiteProvider, useSQLiteContext } from "expo-sqlite"
// <SQLiteProvider databaseName="app.db">{children}</SQLiteProvider>
function TodoList() {
const db = useSQLiteContext()
// db.getAllAsync(...) etc.
}
Gotchas
- Run your
CREATE TABLEmigrations once at startup (they're idempotent withIF NOT EXISTS). - Prefer
runAsync/getAllAsyncover the old callback-based transaction API from older SDKs. - SQLite is for structured data — for a couple of settings use AsyncStorage, and for secrets use expo-secure-store.
Next steps
Combine SQLite with expo-file-system to store file blobs alongside their metadata, and see the complete lineup in the top 10 Expo SDK modules.