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'srawQuery()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()usesSQLiteDatabase.execSQL(), which rejects SQL that returns a result row. Usequery()for PRAGMA statements that report a value (e.g.journal_mode); PRAGMA statements with no rows (e.g.PRAGMA foreign_keys = ON) still work throughexecute(). - 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
awaittransaction 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(). UserunBatch()/runMany()for bulk writes instead of fanning outrun()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()'slastInsertIdis0for any statement with anON CONFLICTclause, not just for statements with zerochanges— SQLite only updates the rowid counter on a genuineINSERT, not theDO UPDATEarm of an upsert. Usequery()withRETURNINGto get the affected row's id. - Ambiguous unchanged rowids.
last_insert_rowid()is connection state, not an unambiguous per-statement result.run()returns0when that counter didn't change — for example after inserting into aWITHOUT ROWIDtable, replacing the same explicit rowid, or reusing a deleted rowid. UseRETURNINGwhenever 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 INSERT → lastInsertId = 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.