/expo-sqlite
Persist and query local data with expo-sqlite in Expo or React Native apps. Use for SQLite setup, SQL queries, schema migrations, transactions, SQLiteProvider, expo-sqlite/kv-store, or the localStorage polyfill.
Claude Code
$ npx -y skills add expo/expo --skill expo-sqlite --agent claude-code
Codex
$ npx skills add expo/expo --skill expo-sqlite -a codex -y
Cursor
$ npx skills add expo/expo --skill expo-sqlite -a cursor -y
Gemini CLI
$ npx skills add expo/expo --skill expo-sqlite -a gemini-cli -y
GitHub Copilot
$ npx skills add expo/expo --skill expo-sqlite -a github-copilot -y
OpenCode
$ npx skills add expo/expo --skill expo-sqlite -a opencode -y
Windsurf
$ npx skills add expo/expo --skill expo-sqlite -a windsurf -y
SKILL.md
expo-sqlite.SKILL.mdname: expo-sqlite
description: Persist and query local data with expo-sqlite in Expo or React Native apps. Use for SQLite setup, SQL queries, schema migrations, transactions, SQLiteProvider, expo-sqlite/kv-store, or the localStorage polyfill.
license: MIT
expo-sqlite
Use SQLite for structured local data, offline queues, and persistent caches. For simple string key-value storage, use `expo-sqlite/kv-store`. Preserve an existing ORM or storage architecture unless the task calls for changing it.
Setup and API selection
Install with `npx expo install expo-sqlite`. Check the installed package version and its types before using newer APIs such as `db.sql`. Use the [SQLite documentation](https://docs.expo.dev/versions/latest/sdk/sqlite/) for the app's SDK version; use [unversioned docs](https://docs.expo.dev/versions/unversioned/sdk/sqlite/) only when working with unreleased code.
The package supports Android, iOS, macOS, tvOS, and web. Standard SQLite is included in Expo Go; custom native build options require rebuilding the app. Web requires additional setup in [references/web-and-config.md](references/web-and-config.md).
| Task | API | | ------------------------------------------------ | ------------------------------------- | | Static DDL or multiple SQL statements | `db.execAsync(source)` | | Write and receive `{ lastInsertRowId, changes }` | `db.runAsync(source, params)` | | Read one row, or `null` if absent | `db.getFirstAsync<T>(source, params)` | | Read a bounded result set | `db.getAllAsync<T>(source, params)` | | Iterate rows without collecting the full result | `db.getEachAsync<T>(source, params)` | | Concise parameterized queries | `` db.sql<T>`SELECT ...` `` | | Reuse a compiled statement | `db.prepareAsync(source)` |
Prefer asynchronous methods for application work. Synchronous database methods can block the JavaScript thread, especially for large queries or migrations.
Bind values, not SQL syntax
`execAsync()` does not bind or escape values. Use it for trusted, static SQL. For data, use variadic arguments, arrays, or named parameters:
import type { SQLiteDatabase } from 'expo-sqlite';
async function addTodo(db: SQLiteDatabase, title: string) {
const result = await db.runAsync('INSERT INTO todos (title) VALUES (?)', title);
return result.lastInsertRowId;
}
async function setCompleted(db: SQLiteDatabase, id: number, completed: boolean) {
await db.runAsync('UPDATE todos SET completed = $completed WHERE id = $id', {
$completed: completed ? 1 : 0,
$id: id,
});
}Bindings represent values, not table names, column names, or SQL fragments. Use fixed SQL or an allowlist for dynamic identifiers. Bind `null` for SQL NULL; serialize objects explicitly. `Uint8Array` and `ArrayBuffer` can be bound to BLOB columns in the current package.
The `db.sql` tag replaces interpolations with bound parameters. Do not put quotes around `${value}` or interpolate an array expecting an expanded `IN` list:
import type { SQLiteDatabase } from 'expo-sqlite';
type Todo = { id: number; title: string; completed: number };
async function findTodo(db: SQLiteDatabase, id: number) {
const sql = db.sql;
return await sql<Todo>`SELECT * FROM todos WHERE id = ${id}`.first();
}Awaiting a tagged `SELECT` returns rows; `.first()` returns a row or `null`, `.each()` iterates rows, and `.values()` returns arrays of column values. Writes without `RETURNING` normally return `SQLiteRunResult`; prefer `runAsync()` when you need unambiguous write metadata. A generic type describes the expected row shape; it does not validate stored data or transform SQLite integers into JavaScript booleans.
React integration and migrations
Mount one stable `SQLiteProvider` above the consumers of a database. Descendants call `useSQLiteContext()` to obtain it. Put initialization in `onInit`, which completes before the children render. Keep `onInit` and options stable so a render does not reopen the database.
This example migrates a new database through versions 1 and 2, and upgrades an existing version 1 database without deleting its rows:
import { SQLiteProvider, type SQLiteDatabase } from 'expo-sqlite';
import type { PropsWithChildren } from 'react';
export function DatabaseProvider({ children }: PropsWithChildren) {
return (
<SQLiteProvider databaseName="app.db" onInit={initializeDatabase}>
{children}
</SQLiteProvider>
);
}
async function initializeDatabase(db: SQLiteDatabase) {
// Connection settings must be applied outside the migration transaction.
await db.execAsync('PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;');
const version = await db.getFirstAsync<{ user_version: number }>('PRAGMA user_version');
if (version === null) {
throw new Error('Could not read the database schema version');
}
if (version.user_version > 2) {
throw new Error('Database schema is newer than this app supports');
}
if (version.user_version === 2) {
return;
}
// onInit gates consumers; no other database work should run during initialization.
await db.withTransactionAsync(async () => {
if (version.user_version < 1) {
await db.execAsync(`
CREATE TABLE todos (id INTEGER PRIMARY KEY NOT NULL, title TEXT NOT NULL);
`);
}
if (version.user_version < 2) {
await db.execAsync('ALTER TABLE todos ADD COLUMN completed INTEGER NOT NULL DEFAULT 0');
}
await db.execAsync('PRAGMA user_version = 2');
});
}Keep schema changes and their `user_version` update in the same transaction so failure rolls both back. WAL generally improves database performance; enable foreign keys on each connection that relies on them. Do not delete an existing database to work around a migration e
Read more
name: expo-sqlite description: Persist and query local data with expo-sqlite in Expo or React Native apps. Use for SQLite setup, SQL queries, schema migrations, transactions, SQLiteProvider, expo-sqlite/kv-store, or the localStorage polyfill. license: MIT
expo-sqlite
Use SQLite for structured local data, offline queues, and persistent caches. For simple string key-value storage, use `expo-sqlite/kv-store`. Preserve an existing ORM or storage architecture unless the task calls for changing it.
Setup and API selection
Install with `npx expo install expo-sqlite`. Check the installed package version and its types before using newer APIs such as `db.sql`. Use the [SQLite documentation](https://docs.expo.dev/versions/latest/sdk/sqlite/) for the app's SDK version; use [unversioned docs](https://docs.expo.dev/versions/unversioned/sdk/sqlite/) only when working with unreleased code.
The package supports Android, iOS, macOS, tvOS, and web. Standard SQLite is included in Expo Go; custom native build options require rebuilding the app. Web requires additional setup in [references/web-and-config.md](references/web-and-config.md).
| Task | API | | ------------------------------------------------ | ------------------------------------- | | Static DDL or multiple SQL statements | `db.execAsync(source)` | | Write and receive `{ lastInsertRowId, changes }` | `db.runAsync(source, params)` | | Read one row, or `null` if absent | `db.getFirstAsync<T>(source, params)` | | Read a bounded result set | `db.getAllAsync<T>(source, params)` | | Iterate rows without collecting the full result | `db.getEachAsync<T>(source, params)` | | Concise parameterized queries | `` db.sql<T>`SELECT ...` `` | | Reuse a compiled statement | `db.prepareAsync(source)` |
Prefer asynchronous methods for application work. Synchronous database methods can block the JavaScript thread, especially for large queries or migrations.
Bind values, not SQL syntax
`execAsync()` does not bind or escape values. Use it for trusted, static SQL. For data, use variadic arguments, arrays, or named parameters:
import type { SQLiteDatabase } from 'expo-sqlite';
async function addTodo(db: SQLiteDatabase, title: string) {
const result = await db.runAsync('INSERT INTO todos (title) VALUES (?)', title);
return result.lastInsertRowId;
}
async function setCompleted(db: SQLiteDatabase, id: number, completed: boolean) {
await db.runAsync('UPDATE todos SET completed = $completed WHERE id = $id', {
$completed: completed ? 1 : 0,
$id: id,
});
}Bindings represent values, not table names, column names, or SQL fragments. Use fixed SQL or an allowlist for dynamic identifiers. Bind `null` for SQL NULL; serialize objects explicitly. `Uint8Array` and `ArrayBuffer` can be bound to BLOB columns in the current package.
The `db.sql` tag replaces interpolations with bound parameters. Do not put quotes around `${value}` or interpolate an array expecting an expanded `IN` list:
import type { SQLiteDatabase } from 'expo-sqlite';
type Todo = { id: number; title: string; completed: number };
async function findTodo(db: SQLiteDatabase, id: number) {
const sql = db.sql;
return await sql<Todo>`SELECT * FROM todos WHERE id = ${id}`.first();
}Awaiting a tagged `SELECT` returns rows; `.first()` returns a row or `null`, `.each()` iterates rows, and `.values()` returns arrays of column values. Writes without `RETURNING` normally return `SQLiteRunResult`; prefer `runAsync()` when you need unambiguous write metadata. A generic type describes the expected row shape; it does not validate stored data or transform SQLite integers into JavaScript booleans.
React integration and migrations
Mount one stable `SQLiteProvider` above the consumers of a database. Descendants call `useSQLiteContext()` to obtain it. Put initialization in `onInit`, which completes before the children render. Keep `onInit` and options stable so a render does not reopen the database.
This example migrates a new database through versions 1 and 2, and upgrades an existing version 1 database without deleting its rows:
import { SQLiteProvider, type SQLiteDatabase } from 'expo-sqlite';
import type { PropsWithChildren } from 'react';
export function DatabaseProvider({ children }: PropsWithChildren) {
return (
<SQLiteProvider databaseName="app.db" onInit={initializeDatabase}>
{children}
</SQLiteProvider>
);
}
async function initializeDatabase(db: SQLiteDatabase) {
// Connection settings must be applied outside the migration transaction.
await db.execAsync('PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;');
const version = await db.getFirstAsync<{ user_version: number }>('PRAGMA user_version');
if (version === null) {
throw new Error('Could not read the database schema version');
}
if (version.user_version > 2) {
throw new Error('Database schema is newer than this app supports');
}
if (version.user_version === 2) {
return;
}
// onInit gates consumers; no other database work should run during initialization.
await db.withTransactionAsync(async () => {
if (version.user_version < 1) {
await db.execAsync(`
CREATE TABLE todos (id INTEGER PRIMARY KEY NOT NULL, title TEXT NOT NULL);
`);
}
if (version.user_version < 2) {
await db.execAsync('ALTER TABLE todos ADD COLUMN completed INTEGER NOT NULL DEFAULT 0');
}
await db.execAsync('PRAGMA user_version = 2');
});
}Keep schema changes and their `user_version` update in the same transaction so failure rolls both back. WAL generally improves database performance; enable foreign keys on each connection that relies on them. Do not delete an existing database to work around a migration e
An open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.
Other skills on expo.
More skills from other plugins.
android-reverse-engine…
Decompile Android APK, XAPK, JAR, and AAR files using jadx or Fernflower/Vineflower. Reverse engineer Android apps, extract HTTP…
add-component
Add a SwiftUI component from ShipSwift. Use when the user says "add component", "add a view", "add X view", "I need a chart",…
Questions about expo-sqlite.
- Which coding agents can use expo-sqlite?
- expo-sqlite installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.

