Docs

Platform notes

Storage paths, the directory option matrix, feature support, and behavioral differences between iOS, Android, Web, and Electron that matter for correctness-sensitive code.

Storage paths

Platform Default path
iOS <Library/Application Support>/CapacitorSQLite/<name>.db
Android <filesDir>/CapacitorSQLite/<name>.db
Web OPFS — file:<name>.db?vfs=opfs (origin-scoped, no custom path support)
Electron app.getPath('userData')/CapacitorSQLite/<name>.db

The directory is created automatically if it does not exist. Raw absolute or relative filesystem paths are not accepted for database — use the directory option below to choose a supported logical location instead.

directory option matrix

directory iOS Android Web Electron Backup expectation
omitted / default Library/Application Support/CapacitorSQLite/<name>.db <filesDir>/CapacitorSQLite/<name>.db OPFS file:<name>.db?vfs=opfs userData/CapacitorSQLite/<name>.db Backed up on iOS/Android by default; Electron userData may be cloud-backed depending on the OS/user environment
library Library/Application Support/CapacitorSQLite/<name>.db <filesDir>/CapacitorSQLite/<name>.db OPFS fallback userData/CapacitorSQLite/<name>.db Same as default; recommended for persistent app databases
documents Documents/CapacitorSQLite/<name>.db app-specific external Documents if available, otherwise <filesDir>/Documents/CapacitorSQLite/<name>.db OPFS fallback Falls back to userData/CapacitorSQLite/<name>.db Backed up on iOS/Android by default; use only for user-document/export-style data
cache Library/Caches/CapacitorSQLite/<name>.db <cacheDir>/CapacitorSQLite/<name>.db OPFS fallback temp/capacitor-sqlite/CapacitorSQLite/<name>.db Not intended for cloud backup; the OS may delete cache data

Web always uses OPFS regardless of directory — browsers don't expose native app directory paths to the plugin. Electron's documents intentionally falls back to userData so an app database isn't placed directly in the user's visible Documents folder. ':memory:' databases ignore directory entirely.

Feature/version support

iOS Android Web Electron
WAL mode Not supported by sqlite-wasm
:memory:
Min version iOS 15 API 24 OPFS VFS + SharedArrayBuffer support; Safari 17+ Electron 40+ (Node 24+)

Cross-platform caveats

  • One statement per SQL string. Each execute().statements[] element, run().statement, runBatch().set[].statement, and each migration statement must contain exactly one SQL statement. Multiple statements packed into one string fails on every platform.
  • Android query() placeholder scanning. Android's rawQuery() only accepts string bind args, so the plugin scans SQL and inlines numeric/boolean/BLOB ? values as SQL literals to preserve type. Only anonymous ? placeholders are supported.
  • Android row-returning PRAGMA statements. Android's execute() uses SQLiteDatabase.execSQL(), which rejects SQL that returns a result row. Use query() for PRAGMA statements that report a value (e.g. journal_mode); PRAGMA statements with no rows (e.g. PRAGMA foreign_keys = ON) still work through execute().
  • Call ordering and multi-database scheduling. On both Android and iOS, every plugin call — across all open databases — is funneled through a single native SQLite thread/serial queue (one executor thread on Android; one serial work queue in front of each database's own connection queue on iOS). Apps with only one open database are unaffected; apps that keep multiple databases open should expect a long operation on one to delay calls to another. Web and Electron each use one underlying SQLite worker per plugin instance, with a separate logical FIFO per database, so a long synchronous SQLite call there can still delay another database's work. Always await transaction boundaries before issuing dependent work. See Concepts.
  • Performance and bridge calls. A manual transaction removes durable-commit cost but not the awaited JS↔native round trip per run(). Use runBatch()/runMany() for bulk writes instead of fanning out run() calls — see Bulk writes.
  • Query planning and indexes. The plugin does not create indexes automatically. Large filtered/sorted queries need application-defined indexes, or SQLite performs a full table scan and temporary sort.
  • UPSERT and lastInsertId. run()'s lastInsertId is 0 for any statement with an ON CONFLICT clause, not just for statements with zero changes — SQLite only updates the rowid counter on a genuine INSERT, not the DO UPDATE arm of an upsert. Use query() with RETURNING to get the affected row's id.
  • Ambiguous unchanged rowids. last_insert_rowid() is connection state, not an unambiguous per-statement result. run() returns 0 when that counter didn't change — for example after inserting into a WITHOUT ROWID table, replacing the same explicit rowid, or reusing a deleted rowid. Use RETURNING whenever the exact affected id matters.

Additional platform-specific observations

From the shared test suite's regression notes:

Observation Platform
Capacitor bridge encodes JS integers as Double-backed NSNumber iOS
sqlite3_column_blob() returns NULL for zero-length BLOBs iOS
:memory: database survives close() due to the connection pool (see Concepts) Android
compileStatement does not support SELECT Android
node:sqlite stores JS numbers as REAL; integer semantics are preserved with BigInt where needed Electron
NOT IN (…, NULL) returns no rows — standard SQL NULL semantics All
Division by zero returns NULL, not an error All
Multiple NULLs in a UNIQUE column are allowed All
Window functions require SQLite ≥ 3.25 All
WITHOUT ROWID INSERTlastInsertId = 0 (no rowid exists) All
true/false stored as INTEGER 1/0, read back as number All
Web WASM logs every SQLite constraint error to the browser console Web

SQL trust boundary

The plugin validates database names and storage directories, and supports parameterized bind values — it does not sandbox arbitrary SQL text. Treat every statement string and every entry in execute().statements/migration statements as trusted application code, never as user-authored input. Bind user data through values, not string concatenation — parameter binding protects values, but does not make an untrusted SQL command string safe.

Next

Troubleshooting walks through common failure symptoms tied to the caveats above.

Last updated on July 17, 2026