Untuk menggunakan SQLite di aplikasi Capacitor, instal plugin SQLite native seperti @capgo/capacitor-fast-sql, buka database sekali saat startup, buat schema dengan migrasi berversi, lalu baca dan tulis dengan SQL berparameter. Panduan ini membahas setup lengkapnya di Capacitor 8 untuk iOS, Android, dan web, termasuk transaksi, enkripsi, lokasi file, serta error yang paling mungkin Anda temui.
Jika Anda hanya butuh beberapa pengaturan, SQLite terlalu berlebihan. Namun, jika Anda menyimpan daftar, catatan, atau data apa pun yang ingin difilter, diurutkan, atau disinkronkan, ini adalah alat yang tepat.
Kapan SQLite menjadi pilihan penyimpanan yang tepat
Aplikasi Capacitor memiliki beberapa tempat untuk menyimpan data. Masing-masing menyelesaikan masalah yang berbeda:
| Penyimpanan | Cocok untuk | Batasan |
|---|---|---|
localStorage / IndexedDB |
Cache sekali pakai | Berada di dalam WebView, OS dapat menghapusnya, tidak ada query nyata di localStorage |
@capacitor/preferences |
Beberapa pengaturan kecil (tema, bahasa) | Hanya nilai string, tidak untuk data besar, tidak terenkripsi |
| Penyimpanan Keychain / Keystore | Token, kunci enkripsi | Hanya secret kecil |
File (@capacitor/filesystem) |
Gambar, PDF, ekspor | Tanpa query |
| SQLite | Record, data offline, antrean sinkronisasi, pencarian | Anda kelola schema dan migrasi |
Pilih SQLite jika Anda butuh salah satu ini: data offline-first, lebih dari beberapa ratus record, filter dan sorting, relasi antar tabel, penulisan multi-langkah atomik, atau enkripsi at-rest untuk data terstruktur.
Memilih plugin Capacitor SQLite di 2026
Ada tiga opsi realistis untuk SQLite native di Capacitor 8:
@capgo/capacitor-fast-sql |
@capacitor-community/sqlite |
@capgo/capacitor-data-storage-sqlite |
|
|---|---|---|---|
| Tujuan | Database SQL umum | Database SQL umum | String key-value store di SQLite |
| Platform | iOS, Android, Web | iOS, Android, Web, Electron | iOS, Android, Web, Electron |
| Mesin web | SQLite Wasm + OPFS Resmi | jeep-sqlite (sql.js + IndexedDB) | IndexedDB |
| Transport data | Channel HTTP lokal, bypass bridge | bridge Capacitor | bridge Capacitor |
| Enkripsi | Opsional SQLCipher (iOS, Android) | SQLCipher selalu dibundel | SQLCipher, passphrase di config |
| Format baris | Objek dengan key kolom | Objek dengan key kolom | String |
| Dukungan ORM | Driver kustom (Kysely, Drizzle) | Driver bawaan TypeORM | Tidak ada |
Panduan ini menggunakan Fast SQL. Panduan ini dipelihara oleh Capgo, mengikuti versi mayor Capacitor (plugin v8 untuk Capacitor 8), menjaga SQLCipher tetap opt-in, dan menghindari serialisasi set hasil besar lewat bridge. Jika Anda sudah memakai plugin komunitas, baca panduan perbandingan dan migrasi kami pertama.
Instal Fast SQL
bun add @capgo/capacitor-fast-sql
bunx cap sync
Fast SQL memulai server HTTP kecil yang terikat ke localhost dan mengirim SQL ke sana dengan bearer token per koneksi. Kedua platform memblokir HTTP cleartext secara default, jadi izinkan hanya untuk loopback.
iOS: izinkan local networking
Tambahkan ini ke ios/App/App/Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
NSAllowsLocalNetworking hanya mencakup loopback dan alamat lokal. Ini tidak melonggarkan App Transport Security untuk panggilan API Anda yang sebenarnya.
Android: izinkan cleartext ke localhost
Buat 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>
Rujukannya di android/app/src/main/AndroidManifest.xml:
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
Jika Anda sudah memiliki network security config (misalnya untuk SSL pinning), tambahkan domain-config blok ke file tersebut alih-alih membuat file kedua.
Buka database satu kali
Buat satu modul yang mengelola koneksi. Membuka database yang sama berulang kali itu boros, dan Anda ingin satu tempat untuk menjalankan migrasi.
// 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 dan performancePresets tersedia mulai dari versi plugin 8.0.49. performancePresets juga mengaktifkan foreign_keys, yang secara default dimatikan oleh SQLite, sehingga ON DELETE CASCADE benar-benar berfungsi.
Panggil getDb() di mana pun Anda membutuhkan database. Panggilan pertama membuka dan melakukan migrasi, panggilan berikutnya menggunakan promise yang sama. Jika pembukaan atau migrasi gagal, panggilan berikutnya akan mencoba lagi.
Schema migrations dengan PRAGMA user_version
Fast SQL memberikan akses ke SQL mentah, bukan framework migrasi, dan itu tidak masalah karena SQLite sudah memiliki slot versi di header file: PRAGMA user_version. Simpan array langkah yang terurut dan terapkan hanya yang belum ada.
// 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}`);
});
}
}
Aturan yang menjaga keamanan ini:
- Satu pernyataan SQL per
execute()call. Kedua implementasi native hanya menyiapkan statement pertama dari string, jadi"CREATE ...; CREATE ..."secara diam-diam menjalankan salah satunya. - Setiap versi berjalan dalam transaksi, jadi jika crash terjadi di tengah jalan, database tetap pada versi sebelumnya dan langkah tersebut akan dijalankan ulang saat peluncuran berikutnya.
- Utamakan perubahan aditif (tabel baru, kolom nullable baru). SQLite mendukung
ALTER TABLE ... DROP COLUMNdanRENAME COLUMNpada versi terbaru, tetapi SQLite sistem di rilis Android lama mungkin tertinggal, jadi cara portabel untuk mengubah bentuk tabel adalah buat tabel baru, salin, hapus yang lama, ganti nama.
Migrations dan live updates
Jika Anda mengirim perubahan JavaScript dengan Live updates Capgo, migrasi code Anda dikirim bersama bundle. Ini praktis, karena perubahan skema tidak memerlukan rilis store. Ini juga berarti rollback dapat menempatkan bundle yang lebih lama di atas skema yang lebih baru. Jaga agar migrasi kompatibel mundur (tambahkan kolom, jangan ganti nama atau hapus kolom dalam rilis yang sama yang berhenti menggunakannya) agar bundle yang lebih lama tetap berfungsi.
Membaca dan menulis data
SQLConnection memberikan Anda empat metode yang akan Anda gunakan terus-menerus:
| Metode | Mengembalikan | Gunakan untuk |
|---|---|---|
query(sql, params) |
SQLRow[] |
Bacaan SELECT dan PRAGMA |
run(sql, params) |
{ rowsAffected, insertId } |
INSERT, UPDATE, DELETE |
execute(sql, params) |
{ rows, rowsAffected, insertId } |
Apa pun, hasil lengkap |
executeBatch(ops) |
SQLResult[] |
Banyak statement dalam satu round trip |
Selalu bind nilai dengan ? placeholder. Jangan pernah menyusun SQL dengan string concatenation dari input pengguna.
// 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;
}
Baris dikembalikan sebagai objek biasa yang di-key berdasarkan nama kolom, jadi tidak ada langkah mapping columns/values. SQLite tidak punya tipe boolean, jadi simpan 1 dan 0 dan konversi di batas data layer Anda, sebagai setPinned melakukan.
Transaksi dan batch write
Bungkus setiap penulisan multi-langkah dalam transaksi. Ini menjaga konsistensi data dan jauh lebih cepat, karena SQLite hanya sinkronisasi ke disk sekali per commit, bukan sekali per statement.
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 saat callback resolve dan roll back jika terjadi error. Jika Anda butuh kontrol manual, ada juga beginTransaction(), commit() dan rollback() pada koneksi.
Semua panggilan berbagi satu koneksi native per database. Saat transaksi terbuka, code lain yang menulis ke database yang sama akan berjalan di dalamnya. Jaga agar transaksi tetap singkat dan hindari await request jaringan di dalamnya.
Menyimpan data biner
Lewati sebuah Uint8Array dan Anda akan mendapatkan Uint8Array kembali. Plugin ini meng-encode base64 untuk transport dan menyimpan BLOB asli.
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;
Simpan media besar (foto, video) sebagai file dan simpan hanya path-nya di SQLite. BLOB cocok untuk thumbnail, lampiran kecil, dan embeddings.
Enkripsi database
Fast SQL mendukung SQLCipher di iOS dan Android. Ini bersifat opt-in, jadi aplikasi yang tidak membutuhkannya tidak akan menyertakannya:
const db = await FastSQL.connect({
database: 'vault',
encrypted: true,
encryptionKey: keyFromSecureStorage,
});
Di Android, Anda menambahkan net.zetetic:sqlcipher-android ke aplikasi Anda build.gradleaplikasi Anda. Di iOS, Anda beralih ke subspec CapgoCapacitorFastSql/SQLCipher pod. Pembuatan kunci, penyimpanannya di Keychain atau Keystore, enkripsi database plaintext yang sudah ada, dan rotasi kunci dijelaskan langkah demi langkah di Cara mengenkripsi database SQLite di Capacitor. Enkripsi tidak tersedia di web.
Dukungan Web
Di web, Fast SQL menggunakan build resmi @sqlite.org/sqlite-wasm dan menyimpan file di Origin Private File System (OPFS). Ini adalah file SQLite asli, bukan salinan yang disimpan di memori lalu di-flush ke IndexedDB.
OPFS membutuhkan cross-origin isolation, jadi dev server dan host produksi Anda harus mengirim dua header:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Dengan 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'],
},
});
Jika header tidak ada, plugin akan mencatat peringatan dan beralih ke database non-persistent: semuanya berfungsi sampai Anda memuat ulang. CapgoCapacitorFastSql.configureWeb({ useOpfs, worker }) memungkinkan Anda mengubah nilai default sebelum pertama kali connect(), dan tidak melakukan apa-apa di native.
require-corp juga memblokir gambar dan skrip lintas asal yang tidak mengirim header CORP atau CORS, jadi uji coba embed pihak ketiga saat mengaktifkannya.
Lokasi penyimpanan file database
| Platform | Lokasi | Catatan |
|---|---|---|
| iOS | Documents/<name>.db |
Termasuk dalam backup iCloud dan perangkat secara default |
| Android | files/<name>.db di penyimpanan internal aplikasi |
Tergantung pada aturan Auto Backup Android |
| Web | OPFS untuk origin halaman | Dihapus jika pengguna menghapus data situs |
Dua konsekuensi praktis. Pertama, uninstall aplikasi akan menghapus file di kedua platform mobile. Kedua, backup dapat memulihkan file database ke perangkat baru. Itu biasanya yang diinginkan, tetapi jika database dienkripsi dengan kunci yang tidak ikut terbawa dalam backup, file yang dipulihkan tidak dapat dibuka. Panduan enkripsi membahas cara menanganinya.
Checklist performa
- Aktifkan
walMode. Pembaca berhenti memblokir penulis. - Kelompokkan penulisan dalam transaksi dengan
executeBatch. Ini adalah keuntungan terbesar untuk impor dan sinkronisasi. - Tambahkan indeks untuk kolom yang Anda filter atau urutkan, lalu periksa dengan
EXPLAIN QUERY PLAN. - Pilih hanya kolom yang Anda butuhkan. Kolom besar
TEXTdanBLOBmemakan waktu transfer bahkan melalui kanal HTTP. - Gunakan paginasi dengan
WHERE created_at < ? ORDER BY created_at DESC LIMIT 50alih-alih query besarOFFSETnilai. - Setelah menghapus banyak data, jalankan
VACUUMsecara berkala untuk mengecilkan file. Proses ini menulis ulang seluruh database, jadi lakukan saat aplikasi idle.
Pemecahan Masalah
SQL execution failed di iOS tepat setelah connect. Permintaan HTTP lokal diblokir. Pastikan NSAllowsLocalNetworking sudah berada di tempat yang tepat Info.plist dan rebuild aplikasi native, live reload saja tidak cukup.
CLEARTEXT communication to localhost not permitted di Android. Konfigurasi keamanan jaringan tidak ada atau tidak dirujuk dari manifest.
Hanya statement pertama yang dijalankan. Anda mengirim beberapa statement dalam satu string. Pisahkan, atau gunakan executeBatch.
no such table setelah update. Migrasi tidak dijalankan, atau dijalankan terhadap nama database lain. Log PRAGMA user_version saat startup dan bandingkan dengan array steps Anda.
Filter pada nilai boolean atau nilai yang dihitung tidak pernah cocok di Android. Untuk query baca, implementasi Android mengikat parameter sebagai teks. Perbandingan terhadap kolom dengan afinitas INTEGER tetap berfungsi karena SQLite mengonversi teksnya, tetapi perbandingan terhadap ekspresi (misalnya json_extract(...) = ? atau count(*) > ?) membandingkan teks dengan angka. Kirim 1/0 alih-alih true/false, dan gunakan CAST(? AS INTEGER) saat membandingkan dengan sebuah ekspresi.
INSERT ... RETURNING tidak mengembalikan baris apa pun di Android. Pada versi plugin saat ini, Android hanya mengembalikan baris untuk statement yang diawali dengan SELECT, PRAGMA atau EXPLAIN. Gunakan insertId dari run() dan pilih barisnya setelah itu. Hal yang sama berlaku untuk query yang diawali dengan WITH: bungkus agar diawali dengan SELECT, atau uji di Android sejak awal.
Data hilang saat reload di browser. OPFS tidak tersedia, hampir selalu karena header COOP/COEP hilang di host yang di-deploy.
Encryption is not available in this build di iOS. Anda melewatkan encrypted: true tanpa beralih ke subspec SQLCipher.
Langkah selanjutnya
Raw SQL cukup untuk aplikasi kecil. Begitu Anda punya lebih dari beberapa tabel, lapisan bertipe membantu:
- Kysely dengan Capacitor dan SQLite: typed query builder dengan driver kustom kecil untuk Fast SQL.
- Drizzle ORM dengan Capacitor dan SQLite: skema dalam TypeScript, migrasi yang di-generate.
- TypeORM dengan Capacitor dan SQLite: decorator dan repository.
- Opsi penyimpanan key-value di Capacitor: saat Preferences atau tabel KV di SQLite sudah cukup.
Referensi plugin-nya ada di Dokumentasi Fast SQL dan halaman plugin. Fast SQL memerlukan rebuild native saat Anda menambahkannya atau mengubah konfigurasinya. Jika Anda tidak ingin mengelola Mac untuk itu, Build Capgo dapat menghasilkan binary iOS dan Android bertanda tangan di cloud. Sejak April 2026, upload ke App Store harus dibangun dengan Xcode 26 atau lebih baru, jadi periksa build machine Anda sebelum rilis berikutnya.