---
description: A practical Capacitor SQLite integration guide — open a database, run migrations and transactions, encrypt data, and store the key securely.
title: How to Use SQLite in a Capacitor App - Capawesome
image: https://capawesome.io/docs/assets/images/social/blog/how-to-use-sqlite-in-a-capacitor-app.png
---

<!doctype html> 

[Skip to content ](#how-to-use-sqlite-in-a-capacitor-app) 

[📲 Introducing **Build Sharing** — get your builds onto testers' devices with a link & QR code. No account required. ](/blog/share-mobile-app-builds-with-testers/) 

* [ SDKs ](/docs/sdks/)
* [ Formbricks ](/docs/sdks/capacitor/formbricks/)
* [ Geocoder ](/docs/sdks/capacitor/geocoder/)
* [ Google Sign-In ](/docs/sdks/capacitor/google-sign-in/)
* [ Grafana Faro ](/docs/sdks/capacitor/grafana-faro/)
* [ Gyroscope ](/docs/sdks/capacitor/gyroscope/)
* [ Haptics ](/docs/sdks/capacitor/haptics/)
* [ Home Indicator ](/docs/sdks/capacitor/home-indicator/)
* [ In-App Browser ](/docs/sdks/capacitor/in-app-browser/)
* [ Install Referrer ](/docs/sdks/capacitor/install-referrer/)
* [ Intercom ](/docs/sdks/capacitor/intercom/)
* [ Intune ](/docs/sdks/capacitor/intune/)
* [ Keep Awake ](/docs/sdks/capacitor/keep-awake/)
* [ libSQL ](/docs/sdks/capacitor/libsql/)
* [ Light Sensor ](/docs/sdks/capacitor/light-sensor/)
* [ Live Update ](/docs/sdks/capacitor/live-update/)
* [ Localization ](/docs/sdks/capacitor/localization/)
* [ Mail Composer ](/docs/sdks/capacitor/mail-composer/)
* [ Managed Configurations ](/docs/sdks/capacitor/managed-configurations/)
* [ Maps Launcher ](/docs/sdks/capacitor/maps-launcher/)
* [ Media Session ](/docs/sdks/capacitor/media-session/)
* [ ML Kit ](/docs/sdks/capacitor/mlkit/)
* [ Navigation Bar ](/docs/sdks/capacitor/navigation-bar/)
* [ Network ](/docs/sdks/capacitor/network/)
* [ NFC ](/docs/sdks/capacitor/nfc/)
* [ Node.js ](/docs/sdks/capacitor/nodejs/)
* [ OAuth ](/docs/sdks/capacitor/oauth/)
* [ Passkeys ](/docs/sdks/capacitor/passkeys/)
* [ Password Autofill ](/docs/sdks/capacitor/password-autofill/)
* [ PDF Generator ](/docs/sdks/capacitor/pdf-generator/)
* [ PDF Viewer ](/docs/sdks/capacitor/pdf-viewer/)
* [ Pedometer ](/docs/sdks/capacitor/pedometer/)
* [ Permissions ](/docs/sdks/capacitor/permissions/)
* [ Phone Dialer ](/docs/sdks/capacitor/phone-dialer/)
* [ Photo Editor ](/docs/sdks/capacitor/photo-editor/)
* [ Photo Manipulator ](/docs/sdks/capacitor/photo-manipulator/)
* [ PixLive ](/docs/sdks/capacitor/pixlive/)
* [ PostHog ](/docs/sdks/capacitor/posthog/)
* [ Printer ](/docs/sdks/capacitor/printer/)
* [ Privacy Screen ](/docs/sdks/capacitor/privacy-screen/)
* [ Proximity Sensor ](/docs/sdks/capacitor/proximity-sensor/)
* [ Purchases ](/docs/sdks/capacitor/purchases/)
* [ RealtimeKit ](/docs/sdks/capacitor/realtimekit/)
* [ Root Detection ](/docs/sdks/capacitor/root-detection/)
* [ Screen Brightness ](/docs/sdks/capacitor/screen-brightness/)
* [ Screen Orientation ](/docs/sdks/capacitor/screen-orientation/)
* [ Screen Reader ](/docs/sdks/capacitor/screen-reader/)
* [ Screenshot ](/docs/sdks/capacitor/screenshot/)
* [ Secure Preferences ](/docs/sdks/capacitor/secure-preferences/)
* [ Settings Launcher ](/docs/sdks/capacitor/settings-launcher/)
* [ Shake ](/docs/sdks/capacitor/shake/)
* [ Silent Mode ](/docs/sdks/capacitor/silent-mode/)
* [ SIM ](/docs/sdks/capacitor/sim/)
* [ SMS Composer ](/docs/sdks/capacitor/sms-composer/)
* [ Speech Recognition ](/docs/sdks/capacitor/speech-recognition/)
* [ Speech Synthesis ](/docs/sdks/capacitor/speech-synthesis/)
* [ Share Target ](/docs/sdks/capacitor/share-target/)
* [ Square Mobile Payments ](/docs/sdks/capacitor/square-mobile-payments/)
* [ SQLite ](/docs/sdks/capacitor/sqlite/)
* [ Superwall ](/docs/sdks/capacitor/superwall/)
* [ System WebView ](/docs/sdks/capacitor/system-webview/)
* [ Tauri ](/docs/sdks/capacitor/tauri/)
* [ Text Interaction ](/docs/sdks/capacitor/text-interaction/)
* [ Text Zoom ](/docs/sdks/capacitor/text-zoom/)
* [ Thermal State ](/docs/sdks/capacitor/thermal-state/)
* [ Toast ](/docs/sdks/capacitor/toast/)
* [ Torch ](/docs/sdks/capacitor/torch/)
* [ Vault ](/docs/sdks/capacitor/vault/)
* [ Volume ](/docs/sdks/capacitor/volume/)
* [ Wallet ](/docs/sdks/capacitor/wallet/)
* [ Wifi ](/docs/sdks/capacitor/wifi/)
* [ YouTube Player ](/docs/sdks/capacitor/youtube-player/)
* [ Zip ](/docs/sdks/capacitor/zip/)
* [ Cordova ](/docs/sdks/cordova/)
* [ Cloud ](/docs/cloud/)
* [ Integrations ](/docs/cloud/live-updates/integrations/)
* Concepts
* Reference
* [ Troubleshooting ](/docs/cloud/live-updates/troubleshooting/)
* [ FAQ ](/docs/cloud/live-updates/faq/)
* [ Native Builds ](/docs/cloud/native-builds/)
* [ Set Up Environments ](/docs/cloud/native-builds/environments/)
* [ Set Up Native Configurations ](/docs/cloud/native-builds/native-configurations/)
* [ Auto-Increment Build Numbers ](/docs/cloud/native-builds/auto-incrementing-build-numbers/)
* [ Configure the Web Build Script ](/docs/cloud/native-builds/web-build-script/)
* [ Build from a Monorepo ](/docs/cloud/native-builds/monorepo/)
* [ Use pnpm, Yarn, or bun ](/docs/cloud/native-builds/package-managers/)
* [ Install Private npm Packages ](/docs/cloud/native-builds/npm-private-registry/)
* [ Override the Java Version ](/docs/cloud/native-builds/override-java-version/)
* [ Custom iOS Provisioning Profiles ](/docs/cloud/native-builds/custom-ios-provisioning-profiles/)
* [ Build without Git ](/docs/cloud/native-builds/build-without-git/)
* [ Access Git Behind a Firewall ](/docs/cloud/native-builds/firewall-access/)
* [ Integrations ](/docs/cloud/native-builds/integrations/)
* Reference
* [ Troubleshooting ](/docs/cloud/native-builds/troubleshooting/)
* [ FAQ ](/docs/cloud/native-builds/faq/)
* [ App Store Publishing ](/docs/cloud/app-store-publishing/)
* [ Submit a Build ](/docs/cloud/app-store-publishing/submit-a-build/)
* [ Submit Automatically After a Build ](/docs/cloud/app-store-publishing/submit-automatically/)
* [ Troubleshooting ](/docs/cloud/app-store-publishing/troubleshooting/)
* [ FAQ ](/docs/cloud/app-store-publishing/faq/)
* [ Automations ](/docs/cloud/automations/)
* [ Reference ](/docs/cloud/automations/reference/)
* [ Troubleshooting ](/docs/cloud/automations/troubleshooting/)
* [ FAQ ](/docs/cloud/automations/faq/)
* [ Assist ](/docs/cloud/assist/)
* [ CLI ](/docs/cloud/cli/)
* APIs and SDKs
* [ Webhooks ](/docs/cloud/webhooks/)
* [ Integrations ](/docs/cloud/integrations/)
* Notifications
* Account
* [ Organization ](/docs/cloud/organizations/)
* [ Two-Factor Enforcement ](/docs/cloud/organizations/two-factor-authentication/)
* [ Audit Logs ](/docs/cloud/organizations/audit-logs/)
* [ Billing ](/docs/cloud/organizations/billing/)
* [ License Keys ](/docs/cloud/license-keys/)
* [ AI ](/docs/ai/)
* [ Insiders ](/docs/insiders/)
* [ Billing & Plans ](/docs/insiders/billing-and-plans/)
* [ FAQ ](/docs/insiders/faq/)
* [ License ](https://capawesome.io/legal/eula/)
* [ Support ](/docs/support/)
* [ Contributing ](/docs/contributing/)
* Contributing code
* [ Code of Conduct ](/docs/contributing/code-of-conduct/)
* [ Questions ](https://docs.github.com/en/discussions/collaborating-with-your-community-using-discussions/participating-in-a-discussion#creating-a-discussion)
* [ Blog ](/blog/)
* Categories

* [ SQLCipher compatibility notes ](#sqlcipher-compatibility-notes)
* [ Platform notes ](#platform-notes)
* [ Common errors and how to fix them ](#common-errors-and-how-to-fix-them)
* [ Performance considerations ](#performance-considerations)
* [ Beyond raw SQL ](#beyond-raw-sql)
* [ Frequently asked questions ](#frequently-asked-questions)
* [ Try Capawesome ](#try-capawesome)
* [ Conclusion ](#conclusion)

* Related links

# How to Use SQLite in a Capacitor App[¶](#how-to-use-sqlite-in-a-capacitor-app "Permanent link")

To use SQLite in a Capacitor app, install the [Capacitor SQLite plugin](/docs/sdks/capacitor/sqlite/), open a database with `open()`, and then read and write rows with `query()` and `execute()`. The plugin gives you one API across Android, iOS, Web, and Electron, so the same code runs everywhere without touching platform-specific database code.

This is the integration guide: we start from an empty project and build up to a production-ready setup — schema migrations with versioning, transactions, an encrypted database, and a secure place to keep the encryption key. Every snippet is copy-paste ready.

[ ![Build and deploy your Capacitor app with Capawesome Cloud](https://capawesome.io/assets/banners/cloud-build-and-deploy-capacitor-apps.png?t=1) ](https://capawesome.io/) 

## Key Takeaways[¶](#key-takeaways "Permanent link")

* **One API, four platforms.** The Capacitor SQLite plugin runs on Android, iOS, Web (via SQLite WASM), and Electron (via `node:sqlite`) with the same TypeScript methods.
* **`query()` returns `columns` and `rows`**, not objects. Rows come back as arrays of values, so you map them to objects yourself (there's a helper below).
* **Schema migrations are declarative.** You pass `upgradeStatements` with a `version` per schema step, and the plugin applies only what's missing based on `PRAGMA user_version`.
* **Encryption is 256-bit AES via SQLCipher, Android and iOS only.** It's opt-in and needs one build-config change per platform.
* **Never hard-code the encryption key.** Generate it once and store it with the [Capacitor Secure Preferences plugin](/docs/sdks/capacitor/secure-preferences/) or, for user-gated access, the [Capacitor Vault plugin](/docs/sdks/capacitor/vault/).

## Prefer to watch? Video walkthrough[¶](#prefer-to-watch-video-walkthrough "Permanent link")

If you'd rather see it built, this video walks through a working Capacitor SQLite setup end to end — opening a database, CRUD operations, and transactions — with the same patterns we cover below.

Want a full project to reference? Two complete demo apps show the same patterns in different frameworks:

* **[Angular Capacitor SQLite Demo](https://github.com/capawesome-team/capacitor-sqlite-angular-demo)** — an Angular app with SQLite CRUD and transaction usage.
* **[React Capacitor SQLite Demo](https://github.com/capawesome-team/capacitor-sqlite-react-demo)** — the same patterns implemented in React.

## When to use SQLite in a Capacitor app[¶](#when-to-use-sqlite-in-a-capacitor-app "Permanent link")

SQLite earns its place whenever an app needs a real local database rather than a handful of stored values. Common use cases:

* **Offline-first apps.** Store structured records on the device so the app stays fully usable with no network, then sync them when connectivity returns — the classic case for a notes app, a field-service tool, or a CRM.
* **Large or relational datasets.** When you need to filter, join, sort, or aggregate, SQL does it in the database instead of you loading everything into memory and looping in JavaScript. This scales to tens of thousands of rows without the UI stalling.
* **Encrypted local storage.** Protect sensitive records — health data, financial history, personal notes — with 256-bit AES encryption on Android and iOS (see [Encrypting the database](#encrypting-the-database)).
* **Full-text search.** Search large amounts of text quickly with SQLite's FTS5 extension instead of scanning strings by hand.
* **Settings and session data.** For small key/value data, the plugin's built-in key-value store persists preferences and session state reliably, without the risk of the WebView clearing `localStorage`.

If all you need is a few encrypted key/value pairs — an auth token, an API key — a full database is more than you need; the [Secure Preferences plugin](/docs/sdks/capacitor/secure-preferences/) is the lighter fit. Reach for SQLite once the shape of your data calls for queries, relations, or larger record sets.

## Installing the plugin[¶](#installing-the-plugin "Permanent link")

To install the Capacitor SQLite plugin, follow the [Installation](/docs/sdks/capacitor/sqlite/#installation) section in the plugin documentation. It covers the npm package, the SQLite WASM dependency for the web, and the per-platform build settings. The [Platform notes](#platform-notes) section further down summarizes the setup gotchas that trip people up most often.

The plugin is available to [Capawesome Insiders](https://capawesome.io/insiders/), and it works in any Capacitor app regardless of framework — Angular, React, Vue, or plain JavaScript.

## Opening a database[¶](#opening-a-database "Permanent link")

Everything starts with a database connection. The [open(...)](/docs/sdks/capacitor/sqlite/#open) method opens an existing database file or creates a new one, and returns a `databaseId` you pass to every other call:

`[](#%5F%5Fcodelineno-0-1)import { Sqlite } from '@capawesome-team/capacitor-sqlite';
[](#%5F%5Fcodelineno-0-2)
[](#%5F%5Fcodelineno-0-3)const { databaseId } = await Sqlite.open({
[](#%5F%5Fcodelineno-0-4)  path: 'notes.sqlite3',
[](#%5F%5Fcodelineno-0-5)  version: 1,
[](#%5F%5Fcodelineno-0-6)  upgradeStatements: [
[](#%5F%5Fcodelineno-0-7)    {
[](#%5F%5Fcodelineno-0-8)      version: 1,
[](#%5F%5Fcodelineno-0-9)      statements: [
[](#%5F%5Fcodelineno-0-10)        'CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, body TEXT, created_at INTEGER)',
[](#%5F%5Fcodelineno-0-11)      ],
[](#%5F%5Fcodelineno-0-12)    },
[](#%5F%5Fcodelineno-0-13)  ],
[](#%5F%5Fcodelineno-0-14)});
`

A few things worth knowing:

* **`path`** is a simple filename on Android, iOS, and Web. The plugin stores it in each platform's default database directory. On Electron you can also pass a subfolder or absolute path.
* **Omitting `path` opens an in-memory database** — handy for tests or short-lived scratch data that doesn't need to survive a restart.
* **`version` and `upgradeStatements`** are how you manage your schema over time. More on that next.

Keep the `databaseId` around (in a service or module-level variable) rather than re-opening the database on every call.

## Reading and writing data[¶](#reading-and-writing-data "Permanent link")

Use [execute(...)](/docs/sdks/capacitor/sqlite/#execute) for anything that changes data — `INSERT`, `UPDATE`, `DELETE` — and always bind values with `?` placeholders instead of string concatenation. That's your protection against SQL injection:

`[](#%5F%5Fcodelineno-1-1)const insertNote = async (databaseId: string, title: string, body: string) => {
[](#%5F%5Fcodelineno-1-2)  const { rowId } = await Sqlite.execute({
[](#%5F%5Fcodelineno-1-3)    databaseId,
[](#%5F%5Fcodelineno-1-4)    statement: 'INSERT INTO notes (title, body, created_at) VALUES (?, ?, ?)',
[](#%5F%5Fcodelineno-1-5)    values: [title, body, Date.now()],
[](#%5F%5Fcodelineno-1-6)  });
[](#%5F%5Fcodelineno-1-7)  return rowId; // The id of the row you just inserted
[](#%5F%5Fcodelineno-1-8)};
[](#%5F%5Fcodelineno-1-9)
[](#%5F%5Fcodelineno-1-10)const deleteNote = async (databaseId: string, id: number) => {
[](#%5F%5Fcodelineno-1-11)  const { changes } = await Sqlite.execute({
[](#%5F%5Fcodelineno-1-12)    databaseId,
[](#%5F%5Fcodelineno-1-13)    statement: 'DELETE FROM notes WHERE id = ?',
[](#%5F%5Fcodelineno-1-14)    values: [id],
[](#%5F%5Fcodelineno-1-15)  });
[](#%5F%5Fcodelineno-1-16)  return changes; // How many rows were removed
[](#%5F%5Fcodelineno-1-17)};
`

To read data, use [query(...)](/docs/sdks/capacitor/sqlite/#query). This is the part that surprises people coming from other libraries: the result is not an array of objects. It's a `columns` array plus a `rows` array, where each row is an array of values in column order:

`[](#%5F%5Fcodelineno-2-1)const result = await Sqlite.query({
[](#%5F%5Fcodelineno-2-2)  databaseId,
[](#%5F%5Fcodelineno-2-3)  statement: 'SELECT id, title, body FROM notes WHERE created_at > ?',
[](#%5F%5Fcodelineno-2-4)  values: [Date.now() - 86_400_000],
[](#%5F%5Fcodelineno-2-5)});
[](#%5F%5Fcodelineno-2-6)
[](#%5F%5Fcodelineno-2-7)console.log(result.columns); // ['id', 'title', 'body']
[](#%5F%5Fcodelineno-2-8)console.log(result.rows);    // [[1, 'First note', 'Hello'], [2, 'Second', null]]
`

That shape is compact and fast, but most of the time you want objects. A small helper turns the result into typed records:

`[](#%5F%5Fcodelineno-3-1)import type { Sqlite } from '@capawesome-team/capacitor-sqlite';
[](#%5F%5Fcodelineno-3-2)
[](#%5F%5Fcodelineno-3-3)type QueryResult = Awaited<ReturnType<typeof Sqlite.query>>;
[](#%5F%5Fcodelineno-3-4)
[](#%5F%5Fcodelineno-3-5)const toObjects = <T = Record<string, unknown>>(result: QueryResult): T[] =>
[](#%5F%5Fcodelineno-3-6)  result.rows.map(
[](#%5F%5Fcodelineno-3-7)    (row) => Object.fromEntries(row.map((value, i) => [result.columns[i], value])) as T,
[](#%5F%5Fcodelineno-3-8)  );
[](#%5F%5Fcodelineno-3-9)
[](#%5F%5Fcodelineno-3-10)// Usage
[](#%5F%5Fcodelineno-3-11)interface Note {
[](#%5F%5Fcodelineno-3-12)  id: number;
[](#%5F%5Fcodelineno-3-13)  title: string;
[](#%5F%5Fcodelineno-3-14)  body: string | null;
[](#%5F%5Fcodelineno-3-15)}
[](#%5F%5Fcodelineno-3-16)
[](#%5F%5Fcodelineno-3-17)const notes = toObjects<Note>(result);
`

Reach for this helper whenever you query, and the rest of your app can work with plain objects.

## Schema migrations with versioning[¶](#schema-migrations-with-versioning "Permanent link")

A real app's schema changes over time — you add a column, create a new table, add an index. The plugin handles this declaratively through `upgradeStatements`. Each entry has a `version` and the statements that bring the schema _to_ that version. On open, the plugin checks the database's current `PRAGMA user_version` and runs only the statements newer than it, in order:

`[](#%5F%5Fcodelineno-4-1)const { databaseId } = await Sqlite.open({
[](#%5F%5Fcodelineno-4-2)  path: 'notes.sqlite3',
[](#%5F%5Fcodelineno-4-3)  version: 3,
[](#%5F%5Fcodelineno-4-4)  upgradeStatements: [
[](#%5F%5Fcodelineno-4-5)    {
[](#%5F%5Fcodelineno-4-6)      version: 1,
[](#%5F%5Fcodelineno-4-7)      statements: [
[](#%5F%5Fcodelineno-4-8)        'CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, body TEXT, created_at INTEGER)',
[](#%5F%5Fcodelineno-4-9)      ],
[](#%5F%5Fcodelineno-4-10)    },
[](#%5F%5Fcodelineno-4-11)    {
[](#%5F%5Fcodelineno-4-12)      version: 2,
[](#%5F%5Fcodelineno-4-13)      statements: ['ALTER TABLE notes ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0'],
[](#%5F%5Fcodelineno-4-14)    },
[](#%5F%5Fcodelineno-4-15)    {
[](#%5F%5Fcodelineno-4-16)      version: 3,
[](#%5F%5Fcodelineno-4-17)      statements: [
[](#%5F%5Fcodelineno-4-18)        'CREATE TABLE IF NOT EXISTS tags (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE)',
[](#%5F%5Fcodelineno-4-19)        'CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes (created_at)',
[](#%5F%5Fcodelineno-4-20)      ],
[](#%5F%5Fcodelineno-4-21)    },
[](#%5F%5Fcodelineno-4-22)  ],
[](#%5F%5Fcodelineno-4-23)});
`

The rules that keep migrations predictable:

* **Never edit a shipped version.** Once `version: 1` is in the wild, its statements have already run on users' devices. To change the schema, add a _new_ version. Rewriting an old one won't re-run it.
* **Bump `version` to the highest schema you define.** If you omit `version`, the plugin uses the latest entry in `upgradeStatements`. Setting it explicitly documents intent and guards against half-finished migrations.
* **Keep each version's statements idempotent where you can** (`IF NOT EXISTS`), so a partially-applied upgrade can be re-run safely.

You can read the current schema version at any time with a plain query — `PRAGMA user_version` — which is useful when debugging a migration that didn't apply.

## Transactions[¶](#transactions "Permanent link")

When several statements have to succeed or fail as a unit — inserting a note and its tags, transferring a value between rows — wrap them in a transaction. [beginTransaction(...)](/docs/sdks/capacitor/sqlite/#begintransaction) opens one, [commitTransaction(...)](/docs/sdks/capacitor/sqlite/#committransaction) makes the changes permanent, and [rollbackTransaction(...)](/docs/sdks/capacitor/sqlite/#rollbacktransaction) undoes everything if a step throws:

`[](#%5F%5Fcodelineno-5-1)const createNoteWithTags = async (databaseId: string, title: string, tags: string[]) => {
[](#%5F%5Fcodelineno-5-2)  await Sqlite.beginTransaction({ databaseId });
[](#%5F%5Fcodelineno-5-3)  try {
[](#%5F%5Fcodelineno-5-4)    const { rowId } = await Sqlite.execute({
[](#%5F%5Fcodelineno-5-5)      databaseId,
[](#%5F%5Fcodelineno-5-6)      statement: 'INSERT INTO notes (title, created_at) VALUES (?, ?)',
[](#%5F%5Fcodelineno-5-7)      values: [title, Date.now()],
[](#%5F%5Fcodelineno-5-8)    });
[](#%5F%5Fcodelineno-5-9)    for (const tag of tags) {
[](#%5F%5Fcodelineno-5-10)      await Sqlite.execute({
[](#%5F%5Fcodelineno-5-11)        databaseId,
[](#%5F%5Fcodelineno-5-12)        statement: 'INSERT OR IGNORE INTO tags (name) VALUES (?)',
[](#%5F%5Fcodelineno-5-13)        values: [tag],
[](#%5F%5Fcodelineno-5-14)      });
[](#%5F%5Fcodelineno-5-15)    }
[](#%5F%5Fcodelineno-5-16)    await Sqlite.commitTransaction({ databaseId });
[](#%5F%5Fcodelineno-5-17)    return rowId;
[](#%5F%5Fcodelineno-5-18)  } catch (error) {
[](#%5F%5Fcodelineno-5-19)    await Sqlite.rollbackTransaction({ databaseId });
[](#%5F%5Fcodelineno-5-20)    throw error;
[](#%5F%5Fcodelineno-5-21)  }
[](#%5F%5Fcodelineno-5-22)};
`

The `try/catch/rollback` pattern is the important part: if any statement fails midway, the rollback restores the database to exactly where it was before `beginTransaction()`. Single, independent statements don't need this wrapping — a lone `INSERT` is already atomic.

## Encrypting the database[¶](#encrypting-the-database "Permanent link")

For sensitive data, the plugin supports **256-bit AES encryption via SQLCipher on Android and iOS**. You opt in by passing an `encryptionKey` when you open the database:

`[](#%5F%5Fcodelineno-6-1)const { databaseId } = await Sqlite.open({
[](#%5F%5Fcodelineno-6-2)  path: 'secure-notes.sqlite3',
[](#%5F%5Fcodelineno-6-3)  encryptionKey: key, // See the next section for where this comes from
[](#%5F%5Fcodelineno-6-4)  version: 1,
[](#%5F%5Fcodelineno-6-5)  upgradeStatements: [
[](#%5F%5Fcodelineno-6-6)    {
[](#%5F%5Fcodelineno-6-7)      version: 1,
[](#%5F%5Fcodelineno-6-8)      statements: ['CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)'],
[](#%5F%5Fcodelineno-6-9)    },
[](#%5F%5Fcodelineno-6-10)  ],
[](#%5F%5Fcodelineno-6-11)});
`

Two things to note:

* **Encryption is Android/iOS only.** It's not available on Web or Electron, and it requires one build-config change per native platform to pull in SQLCipher (see [Platform notes](#platform-notes) and [SQLCipher compatibility](#sqlcipher-compatibility-notes)).
* **You can rotate the key** on an already-open encrypted database with [changeEncryptionKey(...)](/docs/sdks/capacitor/sqlite/#changeencryptionkey), without recreating the file:

`[](#%5F%5Fcodelineno-7-1)await Sqlite.changeEncryptionKey({ databaseId, encryptionKey: newKey });
`

## Storing the encryption key securely[¶](#storing-the-encryption-key-securely "Permanent link")

The encryption is only as strong as the secrecy of the key. A key hard-coded in your JavaScript bundle offers no real protection — anyone can extract it from the installed app. The right pattern is: **generate a random key once, store it in platform-backed secure storage, and read it back on every launch.**

### Option 1: Secure Preferences (background access)[¶](#option-1-secure-preferences-background-access "Permanent link")

The [Capacitor Secure Preferences plugin](/docs/sdks/capacitor/secure-preferences/) stores values encrypted at rest using the Android Keystore and iOS Keychain, and the app can read them at any time without prompting the user. That makes it the natural fit for a database key your app needs on every startup:

`[](#%5F%5Fcodelineno-8-1)import { SecurePreferences } from '@capawesome-team/capacitor-secure-preferences';
[](#%5F%5Fcodelineno-8-2)import { Sqlite } from '@capawesome-team/capacitor-sqlite';
[](#%5F%5Fcodelineno-8-3)
[](#%5F%5Fcodelineno-8-4)const KEY_NAME = 'notes-db-encryption-key';
[](#%5F%5Fcodelineno-8-5)
[](#%5F%5Fcodelineno-8-6)const generateKey = (): string => {
[](#%5F%5Fcodelineno-8-7)  const bytes = new Uint8Array(32); // 256 bits
[](#%5F%5Fcodelineno-8-8)  crypto.getRandomValues(bytes);
[](#%5F%5Fcodelineno-8-9)  return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
[](#%5F%5Fcodelineno-8-10)};
[](#%5F%5Fcodelineno-8-11)
[](#%5F%5Fcodelineno-8-12)const getOrCreateKey = async (): Promise<string> => {
[](#%5F%5Fcodelineno-8-13)  const { value } = await SecurePreferences.get({ key: KEY_NAME });
[](#%5F%5Fcodelineno-8-14)  if (value) {
[](#%5F%5Fcodelineno-8-15)    return value;
[](#%5F%5Fcodelineno-8-16)  }
[](#%5F%5Fcodelineno-8-17)  const key = generateKey();
[](#%5F%5Fcodelineno-8-18)  await SecurePreferences.set({ key: KEY_NAME, value: key });
[](#%5F%5Fcodelineno-8-19)  return key;
[](#%5F%5Fcodelineno-8-20)};
[](#%5F%5Fcodelineno-8-21)
[](#%5F%5Fcodelineno-8-22)const openSecureDatabase = async () => {
[](#%5F%5Fcodelineno-8-23)  const encryptionKey = await getOrCreateKey();
[](#%5F%5Fcodelineno-8-24)  return Sqlite.open({ path: 'secure-notes.sqlite3', encryptionKey });
[](#%5F%5Fcodelineno-8-25)};
`

The first launch generates and persists the key; every launch after that reads the same key back, so the database opens cleanly. Note that on Web, Secure Preferences falls back to unencrypted `localStorage` — fine for development, but don't rely on it for production secrets (and encryption isn't available on Web anyway).

### Option 2: Vault (user-gated access)[¶](#option-2-vault-user-gated-access "Permanent link")

If access to the data should require an explicit user action — a biometric prompt or the device passcode — use the [Capacitor Vault plugin](/docs/sdks/capacitor/vault/) instead. The vault has to be unlocked before you can read the key, which means the database can't be opened until the user authenticates:

`[](#%5F%5Fcodelineno-9-1)import { Vault, VaultType } from '@capawesome-team/capacitor-vault';
[](#%5F%5Fcodelineno-9-2)import { Sqlite } from '@capawesome-team/capacitor-sqlite';
[](#%5F%5Fcodelineno-9-3)
[](#%5F%5Fcodelineno-9-4)const KEY_NAME = 'notes-db-encryption-key';
[](#%5F%5Fcodelineno-9-5)
[](#%5F%5Fcodelineno-9-6)await Vault.initialize({
[](#%5F%5Fcodelineno-9-7)  type: VaultType.Biometric,
[](#%5F%5Fcodelineno-9-8)  title: 'Unlock your notes',
[](#%5F%5Fcodelineno-9-9)  lockAfterBackgrounded: 30_000,
[](#%5F%5Fcodelineno-9-10)});
[](#%5F%5Fcodelineno-9-11)
[](#%5F%5Fcodelineno-9-12)const getKeyFromVault = async (): Promise<string> => {
[](#%5F%5Fcodelineno-9-13)  await Vault.unlock(); // Prompts for biometrics or the device passcode
[](#%5F%5Fcodelineno-9-14)  const { value } = await Vault.getValue({ key: KEY_NAME });
[](#%5F%5Fcodelineno-9-15)  if (value) {
[](#%5F%5Fcodelineno-9-16)    return value;
[](#%5F%5Fcodelineno-9-17)  }
[](#%5F%5Fcodelineno-9-18)  const key = generateKey();
[](#%5F%5Fcodelineno-9-19)  await Vault.setValue({ key: KEY_NAME, value: key });
[](#%5F%5Fcodelineno-9-20)  return key;
[](#%5F%5Fcodelineno-9-21)};
`

Use Secure Preferences when the app needs the key silently in the background, and Vault when you want the data locked behind an authentication prompt. The two can coexist — Secure Preferences for the database key, Vault for a master secret that gates the whole app.

## SQLCipher compatibility notes[¶](#sqlcipher-compatibility-notes "Permanent link")

The plugin's encryption is built on [SQLCipher](https://www.zetetic.net/sqlcipher/), and there are a few things to keep in mind before you ship it:

* **It's opt-in per platform.** On Android you set `capawesomeCapacitorSqliteIncludeSqlcipher = true` in `variables.gradle`; on iOS you pick the `SQLCipher` CocoaPods subspec or enable the `SQLCipher` Swift Package Manager trait. Without that, the plugin builds against plain SQLite and encryption is unavailable.
* **Export compliance is your responsibility.** Shipping an app that bundles SQLCipher means shipping cryptography — you're responsible for any export, re-export, and import regulations that apply in the countries you distribute to.
* **The license requires attribution.** SQLCipher's Community Edition uses a BSD-style license that requires its copyright notice and license text to appear in a **user-accessible location** in your app — for example, an "About" or "Licensing" screen. See the [SQLCipher license](https://www.zetetic.net/sqlcipher/license/) for the exact text.
* **It's compatible with Ionic Secure Storage databases.** Ionic Secure Storage is also SQLCipher-based, so an existing encrypted database can generally be opened with the same key — see the [migration FAQ](#is-this-a-drop-in-replacement-for-ionic-secure-storage) below.

## Platform notes[¶](#platform-notes "Permanent link")

The API is identical across platforms, but the one-time setup differs. Here's what each platform needs on top of the base [installation](/docs/sdks/capacitor/sqlite/#installation).

### Android[¶](#android "Permanent link")

* **Encryption:** set `capawesomeCapacitorSqliteIncludeSqlcipher = true` in your app's `variables.gradle` to bundle SQLCipher.
* **Newer SQLite version:** to bundle a consistent SQLite across devices instead of the system one, set `capawesomeCapacitorSqliteIncludeRequery = true` and add the JitPack repository to `build.gradle`. This is also required to load custom extensions.
* **Proguard:** if you use Proguard, keep the plugin classes with `-keep class io.capawesome.capacitorjs.plugins.** { *; }`.

### iOS[¶](#ios "Permanent link")

* **CocoaPods:** add the `CapawesomeTeamCapacitorSqlite/Plain` pod, or `.../SQLCipher` if you want encryption.
* **Swift Package Manager:** no extra setup for the plain build. For encryption, enable the `SQLCipher` package trait in your Capacitor config (requires Capacitor CLI 8.3.0+ and Xcode 16.3+). This is newer than a lot of older tutorials assume — SPM does support encryption now.

### Web[¶](#web "Permanent link")

* **Dependency and headers:** the web build uses SQLite WebAssembly and needs the `@sqlite.org/sqlite-wasm` package plus two response headers on your dev and production servers:

`[](#%5F%5Fcodelineno-10-1)Cross-Origin-Embedder-Policy: require-corp
[](#%5F%5Fcodelineno-10-2)Cross-Origin-Opener-Policy: same-origin
`

* **Initialize the worker:** on web, call `Sqlite.initialize({ worker })` with the SQLite WASM worker before opening a database. Angular and Vite each need a small build-config tweak, both covered in the docs.
* **No encryption, limited BLOBs:** database encryption isn't available on Web, and BLOB values (arrays of numbers) aren't supported there.

### Electron[¶](#electron "Permanent link")

* **Native SQLite:** Electron uses the Node.js `node:sqlite` module, which requires Node.js 22.5.0 or later (Electron 33+).
* **Storage location:** databases live in the app's `userData` directory by default; pass a subfolder or absolute path to organize them.
* **No encryption:** database encryption isn't supported on Electron.

## Common errors and how to fix them[¶](#common-errors-and-how-to-fix-them "Permanent link")

A handful of errors come up often enough to call out:

* **`no such vfs: opfs`** (Web) — the browser can't instantiate the Origin Private File System because the COOP/COEP headers are missing. Add the two `Cross-Origin-*` headers shown above.
* **`Sqlite.open()` never resolves in production** (Web) — same root cause. It works in dev but hangs once deployed because your host (Netlify, Vercel, Nginx, …) isn't sending the COOP/COEP headers. Check the response headers on your HTML document in DevTools → Network.
* **`No such module 'SQLite'`** (iOS) — the `Plain` or `SQLCipher` pod isn't in your `Podfile`. Add it under the `# Add your Pods here` comment and run `pod install`.
* **SQLite result codes** — errors that come from SQLite expose the numeric result code on `error.data.sqliteCode`, so you can branch on specific failures instead of parsing messages:

`[](#%5F%5Fcodelineno-11-1)try {
[](#%5F%5Fcodelineno-11-2)  await Sqlite.open({ path: '/invalid/path/to.db' });
[](#%5F%5Fcodelineno-11-3)} catch (error) {
[](#%5F%5Fcodelineno-11-4)  if (error.data?.sqliteCode === 14) {
[](#%5F%5Fcodelineno-11-5)    // SQLITE_CANTOPEN: the database file could not be opened
[](#%5F%5Fcodelineno-11-6)  }
[](#%5F%5Fcodelineno-11-7)}
`

## Performance considerations[¶](#performance-considerations "Permanent link")

A few habits keep a local SQLite database fast as it grows:

* **Reclaim space with `VACUUM`.** After deleting large amounts of data, the file doesn't shrink on its own. Run [vacuum(...)](/docs/sdks/capacitor/sqlite/#vacuum) periodically to rebuild the file and defragment it.
* **Use in-memory databases for throwaway work.** Opening without a `path` gives you a fast in-memory database that never touches disk — ideal for tests, caching, or transient computation.
* **Skip return values you don't need.** `execute()` returns the change count and last insert id by default. For bulk inserts where you don't use them, set `returnChanges: false` and `returnRowId: false` to save work.
* **One statement per call.** On Android and Electron, each `execute()` or `query()` runs a single statement — statements joined by `;` won't all run. Call once per statement, wrapped in a transaction for bulk work. Statements that return rows (like `PRAGMA journal_mode = WAL`) must go through `query()`, not `execute()`.
* **Batch writes in a transaction.** Wrapping many inserts in a single transaction is dramatically faster than committing each one individually, because SQLite only flushes to disk once.

## Beyond raw SQL[¶](#beyond-raw-sql "Permanent link")

You don't always have to write SQL by hand:

* **Key-value storage:** the plugin ships a built-in `SqliteKeyValueStore` for simple `get`/`set`/`remove` persistence without any SQL. See [Key-Value Storage Made Simple with the SQLite Plugin](/blog/key-value-storage-made-simple-with-the-sqlite-plugin/).
* **ORMs:** if you prefer a typed query builder or entity models, the plugin works with [Drizzle](/blog/how-to-use-drizzle-orm-with-capacitor-and-sqlite/), [Kysely](/blog/how-to-use-kysely-with-capacitor-and-sqlite/), and [TypeORM](/blog/how-to-use-typeorm-with-capacitor-and-sqlite/).
* **Full-text search and extensions:** for FTS5 and custom SQLite extensions, see [How to Use Custom SQLite Extensions in Capacitor](/blog/how-to-use-custom-sqlite-extensions-with-capacitor/).

For a method-by-method reference of the entire API — including `closeAll()`, `getVersion()`, and every option — read [Exploring the Capacitor SQLite API](/blog/exploring-the-capacitor-sqlite-api/).

## Frequently asked questions[¶](#frequently-asked-questions "Permanent link")

### How do I use SQLite in a Capacitor app?[¶](#how-do-i-use-sqlite-in-a-capacitor-app "Permanent link")

Install the Capacitor SQLite plugin, call `open()` to get a `databaseId`, then use `execute()` for writes and `query()` for reads, passing the `databaseId` to each. Manage your schema with `upgradeStatements` and versioning, wrap multi-step writes in `beginTransaction()`/`commitTransaction()`, and — if you need encryption — open the database with an `encryptionKey` on Android or iOS. The same code runs on Android, iOS, Web, and Electron.

### Why does `query()` not return objects?[¶](#why-does-query-not-return-objects "Permanent link")

By design, for speed and size. `query()` returns a `columns` array and a `rows` array, where each row is an array of values in column order. Convert them to objects with a small mapper (see [Reading and writing data](#reading-and-writing-data)) when your app needs record-shaped data.

### Is database encryption available on every platform?[¶](#is-database-encryption-available-on-every-platform "Permanent link")

No. Encryption uses SQLCipher and is available only on Android and iOS, where it provides 256-bit AES. It's opt-in and needs a per-platform build setting. Web and Electron don't support database encryption.

### Where should I store the encryption key?[¶](#where-should-i-store-the-encryption-key "Permanent link")

Not in your source code. Generate a random key once and keep it in platform-backed secure storage — the [Secure Preferences plugin](/docs/sdks/capacitor/secure-preferences/) for background access, or the [Vault plugin](/docs/sdks/capacitor/vault/) when access should require biometrics or a passcode.

### Is this a drop-in replacement for Ionic Secure Storage?[¶](#is-this-a-drop-in-replacement-for-ionic-secure-storage "Permanent link")

Largely, yes. [Ionic Secure Storage](https://ionic.io/products/secure-storage) sunsets on December 31, 2027, and it's built on an encrypted SQLCipher database — the same foundation this plugin uses. Because both apply SQLCipher's default configuration, an existing database can generally be opened with the same encryption key, though you should verify against your own data before migrating. The step-by-step walkthrough is in [Alternative to the Ionic Secure Storage plugin](/blog/alternative-to-ionic-secure-storage-plugin/).

## Try Capawesome[¶](#try-capawesome "Permanent link")

The fastest way to learn the plugin is to open a database and run a query in your own app. Subscribe below to get new Capacitor and SQLite guides as they land.

[Subscribe to the Capawesome Newsletter](https://capawesome.io/newsletter/)

## Conclusion[¶](#conclusion "Permanent link")

Using SQLite in a Capacitor app comes down to a short list of moves: open a database, manage the schema with versioned `upgradeStatements`, read and write with `query()` and `execute()`, group related writes in transactions, and — when the data is sensitive — encrypt the file and keep the key in secure storage rather than your bundle. Once that foundation is in place, everything else (ORMs, full-text search, key-value storage) builds on top of it.

For the complete method reference, continue with [Exploring the Capacitor SQLite API](/blog/exploring-the-capacitor-sqlite-api/). If you have questions or want to share what you're building, join the [Capawesome Discord server](https://discord.gg/VCXxSVjefW), and subscribe to the [Capawesome newsletter](https://capawesome.io/newsletter/) to keep up with new plugin releases.

August 1, 2026 

Back to top

```json
{
      "@context": "https://schema.org",
      "@type": "BlogPosting",
      "headline": "How to Use SQLite in a Capacitor App",
      "description": "A practical Capacitor SQLite integration guide \u2014 open a database, run migrations and transactions, encrypt data, and store the key securely.",
      "image": "https://capawesome.io/assets/banners/cloud-build-and-deploy-capacitor-apps.png",
      "datePublished": "2026-08-02T00:00:00+00:00",
      "dateModified": "2026-08-02T00:00:00+00:00",
      "author": [
        {
          "@type": "Person",
          "name": "Dayana Jabif",
          "url": "https://github.com/djabif"
        }
      ],
      "publisher": {
        "@type": "Organization",
        "name": "Capawesome",
        "url": "https://capawesome.io",
        "logo": {
          "@type": "ImageObject",
          "url": "https://capawesome.io/assets/images/logo.svg"
        }
      },
      "articleSection": "Capacitor",
      "keywords": ["Capacitor", "Guides", "SDKs"],
      "isPartOf": {
        "@type": "Blog",
        "@id": "https://capawesome.io/blog/#blog"
      },
      "mainEntityOfPage": "https://capawesome.io/blog/how-to-use-sqlite-in-a-capacitor-app/",
      "url": "https://capawesome.io/blog/how-to-use-sqlite-in-a-capacitor-app/"
    }
{
      "@context": "https://schema.org",
      "@type": "BreadcrumbList",
      "itemListElement": [
        {
          "@type": "ListItem",
          "position": 1,
          "name": "Home",
          "item": "https://capawesome.io/"
        },
        {
          "@type": "ListItem",
          "position": 2,
          "name": "Blog",
          "item": "https://capawesome.io/blog/"
        },
        {
          "@type": "ListItem",
          "position": 3,
          "name": "How to Use SQLite in a Capacitor App",
          "item": "https://capawesome.io/blog/how-to-use-sqlite-in-a-capacitor-app/"
        }
      ]
    }
{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "How do I use SQLite in a Capacitor app?", "acceptedAnswer": {"@type": "Answer", "text": "Install the Capacitor SQLite plugin, call open() to get a databaseId, then use execute() for writes and query() for reads, passing the databaseId to each. Manage your schema with upgradeStatements and versioning, wrap multi-step writes in beginTransaction() / commitTransaction(), and — if you need encryption — open the database with an encryptionKey on Android or iOS. The same code runs on Android, iOS, Web, and Electron."}}, {"@type": "Question", "name": "Why does query() not return objects?", "acceptedAnswer": {"@type": "Answer", "text": "By design, for speed and size. query() returns a columns array and a rows array, where each row is an array of values in column order. Convert them to objects with a small mapper (see Reading and writing data) when your app needs record-shaped data."}}, {"@type": "Question", "name": "Is database encryption available on every platform?", "acceptedAnswer": {"@type": "Answer", "text": "No. Encryption uses SQLCipher and is available only on Android and iOS, where it provides 256-bit AES. It's opt-in and needs a per-platform build setting. Web and Electron don't support database encryption."}}, {"@type": "Question", "name": "Where should I store the encryption key?", "acceptedAnswer": {"@type": "Answer", "text": "Not in your source code. Generate a random key once and keep it in platform-backed secure storage — the Secure Preferences plugin for background access, or the Vault plugin when access should require biometrics or a passcode."}}, {"@type": "Question", "name": "Is this a drop-in replacement for Ionic Secure Storage?", "acceptedAnswer": {"@type": "Answer", "text": "Largely, yes. Ionic Secure Storage sunsets on December 31, 2027, and it's built on an encrypted SQLCipher database — the same foundation this plugin uses. Because both apply SQLCipher's default configuration, an existing database can generally be opened with the same encryption key, though you should verify against your own data before migrating. The step-by-step walkthrough is in Alternative to the Ionic Secure Storage plugin."}}], "url": "https://capawesome.io/blog/how-to-use-sqlite-in-a-capacitor-app/"}
```
