Langsung ke konten utama

Cara Pakai SQLite di Aplikasi Capacitor (Panduan 2026)

Pelajari cara menggunakan SQLite di aplikasi Capacitor: pilih plugin, buka database, jalankan migrasi, transaksi, enkripsi, dan dukungan web, lengkap dengan code yang berfungsi.

Logo Capacitor untuk panduan penggunaan SQLite di aplikasi Capacitor

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 COLUMN dan RENAME COLUMN pada 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 TEXT dan BLOB memakan waktu transfer bahkan melalui kanal HTTP.
  • Gunakan paginasi dengan WHERE created_at < ? ORDER BY created_at DESC LIMIT 50 alih-alih query besar OFFSET nilai.
  • Setelah menghapus banyak data, jalankan VACUUM secara 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:

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.

Live updates untuk aplikasi Capacitor

Saat bug web-layer sudah live, kirim perbaikannya lewat Capgo alih-alih menunggu berhari-hari untuk persetujuan app store. Pengguna mendapat update di background, sementara perubahan native tetap mengikuti jalur review normal.

Terbaru dari Blog kami

Capgo memberikan insight terbaik yang Anda butuhkan untuk membuat aplikasi mobile yang benar-benar profesional.