Um SQLite in einer Capacitor-Anwendung zu verwenden, installieren Sie ein natives SQLite-Plugin wie @capgo/capacitor-fast-sql, open a database once at startup, create your schema with versioned migrations, then read and write with parameterized SQL. This guide walks through that full setup on Capacitor 8 for iOS, Android and the web, including transactions, encryption, file locations, and the errors you are most likely to hit.
If you only need a few settings, SQLite is overkill. If you store lists, records, or anything you want to filter, sort, or sync, it is the right tool.
When SQLite ist die richtige Speicherwahl
Capacitor Apps haben mehrere Orte, an denen Daten gespeichert werden können. Sie lösen unterschiedliche Probleme:
| Speicher | Gut für | Gut für |
|---|---|---|
localStorage Grenzen |
/ IndexedDB | Im WebView lebt sie, der Betriebssystem kann sie verwerfen, keine echten Abfragen in localStorage |
@capacitor/preferences |
Einige kleine Einstellungen (Thema, Sprache) | Eine kleine Anzahl von Einstellungen (Thema, Sprache) |
| Schlüsseltresor / Speicher für Zertifikate | Schlüsselkasten / Speicher für Verschlüsselungsschlüssel | Kleine Geheimnisse nur |
Dateien (@capacitor/filesystem) |
Bilder, PDFs, Exporte | Keine Abfragen |
| SQLite | Daten, Offline-Daten, Synchronisationswarte, Suche | Datensätze, Offline-Daten, Synchronisationswarte, Suche |
Pick SQLite when you need any of these: offline-first data, more than a few hundred records, filters and sorting, relations between tables, atomic multi-step writes, or encryption at rest for structured data.
Choosing a Capacitor SQLite plugin in 2026
Es gibt drei realistische Optionen für native SQLite auf Capacitor 8:
@capgo/capacitor-fast-sql |
@capacitor-community/sqlite |
@capgo/capacitor-data-storage-sqlite |
|
|---|---|---|---|
| Purpose | Allgemeine SQL-Datenbank | Allgemeine SQL-Datenbank | String-Schlüsselwertspeicher auf SQLite |
| Plattformen | iOS, Android, Web | iOS, Android, Web, Electron | iOS, Android, Web, Electron |
| Web-Engine | Offizielles SQLite Wasm + OPFS | jeep-sqlite (sql.js + IndexedDB) | IndexedDB |
| Datenübertragung | Lokale HTTP-Kanal, umgeht die Bridge | Capacitor Bridge | Capacitor Bridge |
| Verschlüsselung | SQLCipher optional (iOS, Android) | SQLCipher immer eingebunden | SQLCipher, Passwort in Konfiguration |
| Zeilenformat | Objekte mit Spalten als Schlüssel | Objekte mit Spalten als Schlüssel | Zeichenketten |
| ORM-Unterstützung | Benutzerdefinierte Treiber (Kysely, Drizzle) | TypeORM-integrierter Treiber | Keine |
Diese Anleitung verwendet Fast SQL. Es wird von Capgo unterstützt, verfolgt Capacitor Hauptversionen (Plugin v8 für Capacitor 8), ermöglicht SQLCipher optional und vermeidet die Serialisierung großer Ergebnisse über die Bridge. Wenn Sie bereits das Community-Plugin ausführen, lesen Sie unsere Vergleichs- und Migration-Anleitung erste.
Installieren Sie Fast SQL
bun add @capgo/capacitor-fast-sql
bunx cap sync
Fast SQL startet einen kleinen HTTP-Server, der an localhost und sendet SQL an ihn mit einem pro-Verbindungsbearer-Token. Beide Plattformen blockieren HTTP in Klartext durch Standard, daher erlauben Sie es nur für den Loopback.
iOS: Lokales Netzwerk erlauben
Add this to ios/App/App/Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
NSAllowsLocalNetworking erstellt nur Loopback- und lokale Adressen ab. Es entspannt nicht die App-Transport-Sicherheit für Ihre echten API-Anrufe.
Android: Klartext an localhost erlauben
Erstellen android/app/src/main/res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">localhost</domain>
<domain includeSubdomains="false">127.0.0.1</domain>
</domain-config>
</network-security-config>
Referenzieren Sie es in android/app/src/main/AndroidManifest.xml:
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
Wenn Sie bereits eine Netzwerk-Sicherheitskonfiguration (z.B. für SSL-Zerlegung) haben, fügen Sie die domain-config Block anstatt einer zweiten Datei hinzu.
Öffnen Sie die Datenbank einmal
Erstellen Sie eine einzelne Modul, das die Verbindung besitzt. Das Öffnen derselben Datenbank mehrmals ist unverwertbar, und Sie möchten einen Ort haben, an dem Migrations durchgeführt werden.
// src/db/index.ts
import { FastSQL, type SQLConnection } from '@capgo/capacitor-fast-sql';
import { migrate } from './migrate';
let dbPromise: Promise<SQLConnection> | null = null;
export function getDb(): Promise<SQLConnection> {
if (!dbPromise) {
dbPromise = (async () => {
const db = await FastSQL.connect({
database: 'notes',
walMode: true, // PRAGMA journal_mode = WAL
performancePresets: true, // synchronous=NORMAL, busy_timeout, foreign_keys=ON
});
await migrate(db);
return db;
})().catch((error) => {
dbPromise = null; // let the next call retry instead of reusing the failure
throw error;
});
}
return dbPromise;
}
walMode und performancePresets bereitgestellt werden, ab Version 8.0.49 des Plugins. performancePresets aktiviert auch foreign_keys, die SQLite standardmäßig auslässt, so dass ON DELETE CASCADE tatsächlich funktioniert.
Aufrufen Sie getDb() wo immer Sie die Datenbank benötigen. Der erste Aufruf öffnet und migriert, später aufrufende Anfragen nutzen die gleiche Promise. Wenn der Öffnen oder Migrieren fehlschlägt, versucht der nächste Aufruf es erneut.
Schema-Migrationen mit PRAGMA user_version
Fast SQL gibt Ihnen den direkten Zugriff auf SQL, kein Migrationstool, was in Ordnung ist, da SQLite bereits eine Versionsnummer im Dateikopf hat: PRAGMA user_versionHalte an einer geordneten Liste von Schritten und füge nur das Fehlende hinzu.
// src/db/migrate.ts
import type { SQLConnection } from '@capgo/capacitor-fast-sql';
// Index 0 = version 1, index 1 = version 2, ...
// Never edit a step after it shipped. Add a new one.
const steps: string[][] = [
[
`CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT,
created_at INTEGER NOT NULL
)`,
],
[
`ALTER TABLE notes ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0`,
`CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes (created_at)`,
],
];
export async function migrate(db: SQLConnection): Promise<void> {
const rows = await db.query('PRAGMA user_version');
const current = Number(rows[0]?.user_version ?? 0);
for (let version = current; version < steps.length; version++) {
await db.transaction(async (tx) => {
for (const statement of steps[version]) {
await tx.execute(statement);
}
// PRAGMA does not accept bound parameters, the value is a trusted integer
await tx.execute(`PRAGMA user_version = ${version + 1}`);
});
}
}
Regeln, die dies sicherstellen:
- Eine SQL-Anweisung pro
execute()Aufruf. Beide native Implementierungen bereiten nur die erste Anweisung einer Zeichenkette vor, so dass"CREATE ...; CREATE ..."stillstehend eine davon ausgeführt wird. - Jede Version läuft in einer Transaktion, sodass ein Crash in der Mitte die Datenbank auf der vorherigen Version belässt und der Schritt beim nächsten Start erneut läuft.
- Präferieren Sie additive Änderungen (neue Tabellen, neue nullable Spalten). SQLite unterstützt
ALTER TABLE ... DROP COLUMNundRENAME COLUMNin recent versions, but the system SQLite on older Android releases may be behind, so the portable way to reshape a table is create new table, copy, drop old, rename.
Migrations und Live-Updates
Wenn Sie JavaScript-Änderungen mit Capgo Live-Updates, wird Ihre Migration code mit dem Bundle geliefert. Das ist praktisch, da eine Schemaänderung keinen Speicher-Release erfordert. Es bedeutet auch, dass ein Rollback einen älteren Bundle auf einem neuen Schema legen kann. Halten Sie Migrations rückwärts kompatibel (Fügen Sie Spalten hinzu, fügen Sie sie nicht in der gleichen Release um, die sie nicht mehr verwendet) und ein älteres Bundle funktioniert weiterhin.
Lesen und Schreiben von Daten
SQLConnection geben Ihnen vier Methoden, die Sie alle Zeit verwenden werden:
| Methode | Rückgabe | Verwendung für |
|---|---|---|
query(sql, params) |
SQLRow[] |
SELECT- und PRAGMA-Leserequests |
run(sql, params) |
{ rowsAffected, insertId } |
INSERT-, UPDATE- und DELETE-Anfragen |
execute(sql, params) |
{ rows, rowsAffected, insertId } |
Jederzeit, vollständiger Ergebnis |
executeBatch(ops) |
SQLResult[] |
Viele Anweisungen in einem Rundgang |
Binden Sie immer Werte mit ? Platzhalter. Bauen Sie keine SQL-Anweisungen mit String-Konkatenation aus Benutzereingaben.
// src/db/notes.ts
import { getDb } from './index';
export interface Note {
id: number;
title: string;
body: string | null;
created_at: number;
pinned: number;
}
export async function addNote(title: string, body: string | null): Promise<number> {
const db = await getDb();
const { insertId } = await db.run(
'INSERT INTO notes (title, body, created_at) VALUES (?, ?, ?)',
[title, body, Date.now()],
);
if (insertId == null) throw new Error('Insert did not return an id');
return insertId;
}
export async function listNotes(search = ''): Promise<Note[]> {
const db = await getDb();
const rows = await db.query(
'SELECT id, title, body, created_at, pinned FROM notes WHERE title LIKE ? ORDER BY pinned DESC, created_at DESC',
[`%${search}%`],
);
return rows as unknown as Note[];
}
export async function setPinned(id: number, pinned: boolean): Promise<void> {
const db = await getDb();
await db.run('UPDATE notes SET pinned = ? WHERE id = ?', [pinned ? 1 : 0, id]);
}
export async function deleteNote(id: number): Promise<number> {
const db = await getDb();
const { rowsAffected } = await db.run('DELETE FROM notes WHERE id = ?', [id]);
return rowsAffected;
}
Zeilen kommen als einfache Objekte zurück, die durch Spaltennamen gekennzeichnet sind, daher gibt es keine Spalten/Werte-Abstimmung. SQLite kennt keine boolesche Typen, also speichern Sie 1 und 0 und wandeln Sie sie am Rand Ihres Datenlayers um, wie setPinned tun.
Transaktionen und Batch-Schreibvorgänge
Umfasst jede mehrschrittige Schreiboperation eine Transaktion. Sie hält die Daten konsistent und ist viel schneller, da SQLite nur einmal pro Commit auf das Disk-System schreibt, anstatt einmal pro Anweisung.
import { getDb } from './index';
export async function importNotes(items: { title: string; body: string }[]) {
const db = await getDb();
const now = Date.now();
await db.transaction(async (tx) => {
await tx.executeBatch(
items.map((item) => ({
statement: 'INSERT INTO notes (title, body, created_at) VALUES (?, ?, ?)',
params: [item.title, item.body, now],
})),
);
});
}
transaction() commits when the callback resolves and rolls back if it throws. If you need manual control there is also beginTransaction(), commit() und rollback() auf der Verbindung.
All Anfragen nutzen eine einzelne native Verbindung pro Datenbank. Während eine Transaktion offen ist, werden andere code-Anfragen, die dieselbe Datenbank schreiben, innerhalb dieser Transaktion ausgeführt. Halten Sie Transaktionen kurz und vermeiden Sie es, innerhalb von ihnen Netzwerk-Anfragen abzuwarten.
Speichern Sie binäre Daten
Passen Sie ein Uint8Array und Sie erhalten ein Uint8Array zurück. Der Plugin kodiert es bas64 um für den Transport und speichert einen echten BLOB.
const db = await getDb();
await db.execute('CREATE TABLE IF NOT EXISTS thumbnails (note_id INTEGER PRIMARY KEY, data BLOB)');
await db.run('INSERT OR REPLACE INTO thumbnails (note_id, data) VALUES (?, ?)', [noteId, bytes]);
const [row] = await db.query('SELECT data FROM thumbnails WHERE note_id = ?', [noteId]);
const image = row?.data as Uint8Array | undefined;
Halten Sie große Medien (Fotos, Videos) als Dateien und speichern Sie nur den Pfad in SQLite. BLOBs sind gut für Miniaturansichten, kleine Anhänge und Embeddings geeignet.
Verschlüsseln Sie die Datenbank
Fast SQL unterstützt SQLCipher auf iOS und Android. Es ist optional, daher schicken Apps, die es nicht benötigen, es nicht mit:
const db = await FastSQL.connect({
database: 'vault',
encrypted: true,
encryptionKey: keyFromSecureStorage,
});
Bei Android fügen Sie net.zetetic:sqlcipher-android zu Ihrer App’s build.gradleBei iOS wechseln Sie die Pod zu der CapgoCapacitorFastSql/SQLCipher subspezifikation. Die Erstellung der Schlüssel, das Speichern in der Keychain oder Keystore, die Verschlüsselung einer bestehenden plaintext-Datenbank und das Rotieren von Schlüsseln werden in einzelnen Schritten abgedeckt. How Capacitor eine SQLite-Datenbank zu verschlüsseln. Die Verschlüsselung ist auf der Web nicht verfügbar
Web-Unterstützung
Im Web verwendet Fast SQL die offizielle @sqlite.org/sqlite-wasm und speichert das Datei im Origin Private File System (OPFS). Es ist eine echte SQLite-Datei, nicht eine im Speicher gehaltene Kopie, die in IndexedDB geschrieben wird.
OPFS benötigt Cross-Origin-Isolation, daher müssen Ihr Entwicklungs-Server und Ihr Produktions-Host zwei Header senden:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Mit Vite:
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
server: {
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
},
optimizeDeps: {
exclude: ['@sqlite.org/sqlite-wasm'],
},
});
Wenn die Header fehlen, meldet der Plugin eine Warnung und fällt auf eine nicht-persistente Datenbank zurück: Alles funktioniert, bis Sie neu laden. CapgoCapacitorFastSql.configureWeb({ useOpfs, worker }) erlaubt Ihnen, die Standards vor der ersten connect(), und es ist ein No-Op auf nativem.
require-corp Wenn Sie es aktivieren, blockieren auch Quellen, die keine CORP- oder CORS-Header senden, also testen Sie dritte Einbettungen, wenn Sie es aktivieren.
Wo die Datenbank-Datei lebt
| Plattform | Standort | Hinweise |
|---|---|---|
| iOS | Documents/<name>.db |
Eingeschränkt auf iCloud und Geräte-Backup durch Standard |
| Android | files/<name>.db im internen Speicher des Apps |
Unterworfen den Android Auto Backup-Regeln |
| Website | OPFS für die Ursprungsseite | Gelöscht, wenn der Benutzer die Site-Daten löscht |
Zwei praktische Konsequenzen. Zuerst wird das File beim Entfernen der App auf beiden mobilen Plattformen gelöscht. Zweitens können Backups das Datenbank-File auf einen neuen Gerät wiederherstellen. Das ist normalerweise das, was Sie wollen, aber wenn die Datenbank mit einem Schlüssel verschlüsselt ist, der nicht mit dem Backup reist, kann das wiederhergestellte File nicht geöffnet werden. Die Verschlüsselungshinweise decken ab, wie Sie damit umgehen.
Leistungscheckliste
- Ein
walMode. Leser blockieren den Schreiber nicht. - Batcheschreiben in einer Transaktion mit
executeBatch. Dies ist der größte Gewinn für Importe und Synchronisierung. - Indexe für Spalten hinzufügen, die Sie filtern oder sortieren, und überprüfen Sie dann mit
EXPLAIN QUERY PLAN. - Wählen Sie nur die Spalten aus, die Sie benötigen. Große
TEXTundBLOBSpalten kosten auch Übertragungszeit, selbst über den HTTP-Kanal. - Paginieren mit
WHERE created_at < ? ORDER BY created_at DESC LIMIT 50anstatt großenOFFSETWerten. - Nachdem Sie viel Daten gelöscht haben, führen Sie
VACUUMgelegentlich aus, um die Datei zu verkleinern. Es überarbeitet die gesamte Datenbank, also tun Sie es während der Leerlaufzeit.
Troubleshooting
SQL execution failed nachdem Sie sich bei iOS verbunden haben. auf iOS direkt nach dem Verbinden. NSAllowsLocalNetworking ist richtig Info.plist und die native App neu erstellen, eine Live-Neuladung reicht nicht aus.
CLEARTEXT communication to localhost not permitted auf Android. Die Netzwerksicherheitskonfiguration fehlt oder wird nicht aus der Manifestdatei referenziert.
Nur die erste Anweisung wurde ausgeführt. You passed several statements in one string. Split them, or use executeBatch.
no such table nach einer Aktualisierung. Die Migration wurde nicht ausgeführt oder wurde gegen eine andere Datenbank durchgeführt. Protokollieren PRAGMA user_version bei dem Start und vergleichen Sie es mit Ihrem Schritte-Array.
Ein Filter auf eine boolesche oder berechnete Werte trifft nie auf Android. Bei Leserequests bindet die Android-Implementierung Parameter als Text. Vergleiche gegen eine Spalte mit INTEGER-Affinität funktionieren immer noch, da SQLite den Text umwandelt, aber Vergleiche gegen Ausdrücke (z.B. json_extract(...) = ? oder count(*) > ?) vergleiche Text mit einer Zahl. Pass 1/0 oder true/false, und verwenden CAST(? AS INTEGER) wenn Sie es mit einer Ausdruck vergleichen.
INSERT ... RETURNING gibt keine Zeilen auf Android zurück. In der aktuellen Pluginversion gibt Android nur Zeilen zurück, die mit SELECT, PRAGMA oder EXPLAIN. Use insertId von run() und wählen Sie die Zeile danach aus. Das Gleiche gilt für Abfragen, die mit WITHbeginnen: Um sie so zu umschreiben, dass sie mit SELECTbeginnen, oder testen Sie sie bereits auf Android.
Daten verschwinden beim Neuladen im Browser. OPFS ist nicht verfügbar, fast immer, weil die COOP/COEP-Kopfzeilen auf dem bereitgestellten Host fehlen.
Encryption is not available in this build auf iOS. Sie haben encrypted: true ohne auf die SQLCipher-Unterpakete umzuschalten.
Geht es weiter
Roh-SQL ist für kleine Apps in Ordnung. Sobald Sie mehr als ein paar Tabellen haben, hilft eine abgetastete Ebene:
- Kysely mit Capacitor und SQLiteEine abgeleitete Abfrageschreiber mit einem kleinen benutzerdefinierten Treiber für Fast SQL.
- Drizzle ORM mit Capacitor und SQLite: Schema in TypeScript, generierte Migrationen.
- TypeORM mit Capacitor und SQLite: Dekoratoren und Repositories.
- Optionen für Schlüssel-Wert-Speicher in Capacitor: Wenn Preferences oder eine KV-Tabelle in SQLite ausreicht.
Die Plugin-Referenz befindet sich im Fast SQL-Dokumentation und der Plugin-SeiteFast SQL benötigt eine native Rekonstruktion, wenn Sie es hinzufügen oder seine Konfiguration ändern. Wenn Sie nicht wollen, dass Sie einen Mac für das halten müssen, Capgo Build Kann signierte iOS- und Android-Binärdateien im Cloud produzieren. Da seit April 2026 App Store-Uploads mit Xcode 26 oder später erstellt werden müssen, überprüfen Sie bitte Ihr Build-System vor der nächsten Veröffentlichung.