expo-sqlite
This skill enables SQLite database operations in Expo and React Native apps for structured data storage, offline queues, and persistent caches.
Identity proven by GitHub's verified organisation badge (expo)
- Agents
- Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode, claude.ai and API
- License
- MIT (permissive)
- Pin
- c97dc85pinned Oct 9, 2026
- Context cost
- about 2,600 tokenswhen the skill loads
- GitHub stars
- 52,602on expo/expo
- Scripts
- None found
What it does
“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.”
Generated summary from the skill's own text
This skill enables SQLite database operations in Expo and React Native apps for structured data storage, offline queues, and persistent caches. It provides async APIs for queries, transactions, schema migrations, and React integration via SQLiteProvider, plus a key-value store and localStorage polyfill for simpler string storage.
When to use it
- Building offline-capable apps needing structured local data persistence
- Implementing complex queries with transactions and schema versioning
- Storing offline sync queues or user data locally in React Native apps
- Replacing simple string storage with SQLite-backed key-value store or localStorage
When not to
- App already uses a different ORM or storage layer without migration requirement
- Only storing small amounts of unstructured string data in key-value format
- Sensitive data requiring encryption, not plain SQLite storage
Install
These commands install from the publisher's repository and need no account.
skills CLI
npx skills add expo/expo --skill expo-sqliteInstalls from the repository's default branch, which may have changed since the commit we checked.
GitHub CLI 2.90 or later
gh skill install expo/expo expo-sqlite@c97dc85eecd5bbd3dd1b77f15ed9cb2a2f92ae92Installs the commit we checked (c97dc85).
Copy by hand
Download the folder at c97dc85 and put it in .claude/skills/expo-sqlite for Claude Code and in .agents/skills/expo-sqlite for Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode.
Claude Code plugin
/plugin install expo@claude-plugins-officialThe publisher's plugin in Claude's official marketplace. It may hold more than this skill, and we have not pinned it.
Copy for your agent
A prompt for any coding agent. It installs this skill from GitHub at the pinned commit, copies the files as they are, and runs nothing unless you ask.
Prompt
Install the agent skill "expo-sqlite" by Expo. Source, pinned to commit c97dc85eecd5bbd3dd1b77f15ed9cb2a2f92ae92: https://github.com/expo/expo/tree/c97dc85eecd5bbd3dd1b77f15ed9cb2a2f92ae92/packages/expo-sqlite/skills/expo-sqlite 1. Download that folder at exactly this commit. Do not use the default branch. 2. Copy every file unchanged, keeping the folder structure, into .claude/skills/expo-sqlite/ for Claude Code and .agents/skills/expo-sqlite/ for Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode. Create the folders if they do not exist. 3. Do not edit the files, and do not run any script in the folder unless I ask you to. 4. When you are done, list the files you copied and where you put them. Listing and checks: https://www.agentnative.dev/skills/expo/expo-sqlite
SKILL.md at the pin
---
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:
```ts
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:
```ts
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:
```tsx
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 error.
For Suspense, wrap a provider with `useSuspense` in `<Suspense fallback={...}>` and handle failures with an error boundary. `onError` cannot be combined with `useSuspense`. Outside React, share an `openDatabaseAsync()` initialization promise and close the database when its owner is done; do not close the provider's database from a child.
`useSQLiteContext()` exposes the database, not reactive query results. Refresh local state or invalidate the app's query cache after writes. For external changes, see the listener guidance in [references/web-and-config.md](references/web-and-config.md).
## Transactions and concurrent work
`withTransactionAsync()` commits on success and rolls back on a rejected callback. Queries started elsewhere on the same connection while it is active also join the transaction. Await every operation; do not launch unrelated work during initialization.
On native platforms, use `withExclusiveTransactionAsync()` when unrelated queries must stay outside a transaction. Run all transaction queries through `txn`, including queries in helper functions:
```ts
import type { SQLiteDatabase } from 'expo-sqlite';
async function completeTodo(db: SQLiteDatabase, id: number) {
await db.withExclusiveTransactionAsync(async (txn) => {
await txn.runAsync('UPDATE todos SET completed = ? WHERE id = ?', 1, id);
});
}
```
This API is **not supported on web**. It opens a separate connection; competing writes can fail with `database is locked`, rather than being automatically queued. On web, serialize access through the app's database owner and use `withTransactionAsync()`; a platform switch alone does not provide equivalent isolation. Avoid nested transaction helpers.
## Prepared statements
Prepare once for repeated execution and always finalize, including on failure:
```ts
import type { SQLiteDatabase } from 'expo-sqlite';
async function insertTodos(db: SQLiteDatabase, titles: string[]) {
const statement = await db.prepareAsync('INSERT INTO todos (title) VALUES ($title)');
try {
for (const title of titles) {
await statement.executeAsync({ $title: title });
}
} finally {
await statement.finalizeAsync();
}
}
```
For an atomic batch, call this helper with the database or `txn` belonging to a surrounding transaction. `executeAsync<T>()` returns metadata and a cursor with `getFirstAsync()`, `getAllAsync()`, and async iteration. Call `resetAsync()` before reading the same result from the beginning again. Consuming a cursor and then calling `getAllAsync()` without resetting is not a fresh query.
## Key-value storage and localStorage
### AsyncStorage-compatible storage
For AsyncStorage-compatible string storage:
```ts
import Storage from 'expo-sqlite/kv-store';
await Storage.setItem('settings', JSON.stringify({ theme: 'dark' }));
const saved = await Storage.getItem('settings'); // string | null
```
Synchronous methods such as `getItemSync()` are also available. Changing an AsyncStorage import changes the backend; it **does not migrate existing data**. For an existing app, explicitly copy needed keys from the old store, verify the copy, and make the migration safe to retry before removing that dependency. Preserve the user's choice of backend when a migration was not requested.
### localStorage
For shared web-style code, import the polyfill once in the app's entry point before code that uses `localStorage`:
```ts
import 'expo-sqlite/localStorage/install';
globalThis.localStorage.setItem('settings', JSON.stringify({ theme: 'dark' }));
const saved = globalThis.localStorage.getItem('settings'); // string | null
globalThis.localStorage.removeItem('settings');
```
This installs SQLite-backed `globalThis.localStorage` on native platforms. The import is a no-op on web and is excluded from production web bundles, where the browser's existing `localStorage` remains in use. The API is synchronous and stores strings; serialize objects explicitly. Plain SQLite, the key-value store, and this polyfill are not encrypted secret storage.
## Debugging and verification
In development, press **Shift+M** in the Expo CLI terminal and select **Open expo-sqlite** to inspect tables and run queries. Treat edits there as writes to the app's actual database.
For storage changes, verify a fresh install, reopening with persisted data, upgrading an older schema, and rollback after a deliberately failed migration. Check empty query results and strings containing quotes. Test concurrent writes when changing transaction handling, and test web in its configured browser environment when web is a target.
Read [references/web-and-config.md](references/web-and-config.md) for web setup, native build options, SQLCipher, bundled databases, App Groups, change listeners, and database backups.
From expo/expo at c97dc85, shown as plain text under its license: MIT (permissive). The license text is in packages/expo-sqlite/skills/expo-sqlite/SKILL.md.
View SKILL.md on GitHub at c97dc85Files
- SKILL.md11 KB · 7a1bb6ac1fea
- references/
- web-and-config.md7.4 KB · f6babc63ab22
2 files, 18 KB in total. Each file has a sha256 in the pin's manifest; the first 12 characters are shown.
Skill folder on GitHub at c97dc85More from Expo
expo-app-intents
This skill adds Apple App Intents to Expo apps, enabling Siri voice commands and Shortcuts automation through Swift declarations that dispatch to JavaScript…
Claude Code, Codex, Cursor +4 · about 3.6K tokens · pinned 6 days ago
eas-app-stores
This skill configures and executes iOS and Android app builds and store submissions using EAS, Expo's app distribution service.
Claude Code, Codex, Cursor +3 · about 1.7K tokens · pinned 6 days ago
eas-hosting
This skill deploys Expo web applications and API routes to EAS Hosting, a managed edge platform running on Cloudflare Workers.
Claude Code, Codex, Cursor +3 · about 2.7K tokens · pinned 6 days ago
eas-observe
EAS Observe tracks startup, navigation, and custom-event performance in production Expo apps through instrumentation (AppMetricsRoot/ObserveRoot wrapper…
Claude Code, Codex, Cursor +3 · about 1.1K tokens · pinned 6 days ago
eas-simulator
Runs a user's app on a remote iOS or Android simulator hosted on EAS cloud infrastructure, accessible from machines without local simulator support like Linux…
Claude Code, Codex, Cursor +3 · about 6.3K tokens · pinned 6 days ago
eas-update
Configures and publishes over-the-air JavaScript and asset updates using EAS Update and the EAS CLI.
Claude Code, Codex, Cursor +3 · about 2.3K tokens · pinned 6 days ago
Related in Data and databases
add-datasource
Routes users to the correct data source integration for Expo and React Native Power Apps by understanding their data needs and recommending Dataverse…
by MicrosoftOfficial publisher
Claude Code · about 1.4K tokens · pinned 6 days ago
prisma-8
This skill helps agents work with Prisma 8, a contract-first data layer for TypeScript projects.
by PrismaOfficial publisher
Contains scripts
Claude Code, Codex, Cursor +3 · about 3.5K tokens · pinned 6 days ago
prisma-next-queries
Write queries against Postgres, SQLite, or MongoDB using Prisma Next.
by PrismaOfficial publisher
Claude Code, Codex, Cursor +4 · about 3.5K tokens · pinned 6 days ago
prisma-next-runtime
This skill covers configuring and wiring the Prisma Next database runtime in db.ts, including choosing between Postgres, SQLite, or MongoDB facades, composing…
by PrismaOfficial publisher
Claude Code, Codex, Cursor +4 · about 6.1K tokens · pinned 6 days ago
prisma-orm-setup
This skill sets up Prisma ORM in new applications, defaulting to version 8, or connects and troubleshoots existing Prisma 6, 7, or 8 applications.
by PrismaOfficial publisher
Claude Code, Codex, Cursor +4 · about 1.6K tokens · pinned 6 days ago
magic-data-loading
This skill loads data from files (CSV, TSV, Parquet, JSON, JSONL, Excel), databases (SQLite, PostgreSQL, MySQL), and HuggingFace datasets.
by Votee-AICommunity
Contains scripts
Claude Code, Codex, Cursor +3 · about 5.3K tokens · pinned 6 days ago