Para usar SQLite en una aplicación Capacitor, instala un plugin de SQLite nativo como @capgo/capacitor-fast-sqlAbre una base de datos una vez al inicio, crea tu esquema con migraciones versionadas, luego lee y escribe con SQL parametrizado. Esta guía te guía a través de ese conjunto completo en Capacitor 8 para iOS, Android y la web, incluyendo transacciones, cifrado, ubicaciones de archivos y los errores que es probable que encuentres.
Si solo necesitas unos pocos ajustes, SQLite es excesivo. Si almacenas listas, registros o cualquier cosa que desees filtrar, ordenar o sincronizar, es la herramienta adecuada.
When SQLite es la elección de almacenamiento adecuada
Las aplicaciones Capacitor tienen varios lugares para almacenar datos. Resuelven diferentes problemas:
| Almacenamiento | Bueno para | Bueno para |
|---|---|---|
localStorage Límites |
/ IndexedDB | Vive dentro de la WebView, el sistema operativo puede desalojarlo, no hay consultas reales en localStorage |
@capacitor/preferences |
Un par de ajustes pequeños (tema, idioma) | Valores de cadena solo, no destinados a grandes cantidades de datos, no cifrados |
| Almacenamiento de claves de cadena | Tokens, claves de cifrado | Secretos pequeños solo |
Archivos (@capacitor/filesystem) |
Imágenes, PDFs, exportaciones | No consultas |
| SQLite | Registros, datos offline, colas de sincronización, búsqueda | Registros, datos offline, colas de sincronización, búsqueda |
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
Hay tres opciones realistas para SQLite nativo en Capacitor 8:
@capgo/capacitor-fast-sql |
@capacitor-community/sqlite |
@capgo/capacitor-data-storage-sqlite |
|
|---|---|---|---|
| Purpose | Base de datos SQL general | Base de datos SQL general | Almacenamiento de claves de cadena en SQLite |
| Plataformas | iOS, Android, Web | iOS, Android, Web, Electron | iOS, Android, Web, Electron |
| Motor de la web | SQLite Oficial Wasm + OPFS | jeep-sqlite (sql.js + IndexedDB) | IndexedDB |
| Transporte de datos | Canales HTTP locales, evitan el puente | Capacitor puente | Capacitor puente |
| Encriptación | SQLCipher (opcional, iOS, Android) | SQLCipher siempre incluido | SQLCipher, contraseña en configuración |
| Formato de fila | Objetos claveados por columna | Objetos claveados por columna | Strings |
| Soporte de ORM | Controladores personalizados (Kysely, Drizzle) | TypeORM driver integrado | No |
Esta guía utiliza Fast SQL. Está mantenido por Capgo, sigue Capacitor versiones principales (plugin v8 para Capacitor 8), mantiene la opción de SQLCipher y evita serializar conjuntos de resultados grandes a través de la puente. Si ya ejecutas el plugin de la comunidad, lee nuestra guía de comparación y migración Guía de comparación y migración primero.
Instalar Fast SQL
bun add @capgo/capacitor-fast-sql
bunx cap sync
Fast SQL inicia un pequeño servidor HTTP vinculado a localhost y envía SQL a ella con un token de conexión por cada conexión. Ambas plataformas bloquean HTTP en claro por defecto, por lo que permita solo para bucle de vuelta.
iOS: permitir la red local
Agregue esto a ios/App/App/Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
NSAllowsLocalNetworking solo cubre direcciones de bucle y locales. No relaja la seguridad de transporte de aplicaciones para tus llamadas de API reales.
Android: permitir texto claro a localhost
Crear 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>
Referencíalo en android/app/src/main/AndroidManifest.xml:
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
Si ya tienes una configuración de seguridad de red (por ejemplo, para la pinificación de SSL), agrega el domain-config bloque a ella en lugar de crear un archivo secundario.
Abre la base de datos una vez
Crear un módulo único que propietario de la conexión. Abrir la misma base de datos repetidamente es inútil, y quieres un lugar para ejecutar las migraciones.
// 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 y performancePresets Están disponibles desde la versión del plugin 8.0.49. performancePresets también activa foreign_keys, que SQLite omite por defecto, por lo que ON DELETE CASCADE funciona realmente.
Llamar a getDb() dondequiera que necesites la base de datos. La primera llamada abre y migra, las llamadas posteriores reutilizan la misma promesa. Si la apertura o la migración falla, la siguiente llamada intenta de nuevo.
Las migraciones de esquema con PRAGMA user_version
Fast SQL te da SQL bruto, no es un marco de migración, lo cual está bien porque SQLite ya tiene una versión en el encabezado del archivo: PRAGMA user_versionConserva un array ordenado de pasos y aplica solo lo que falta.
// 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}`);
});
}
}
Las reglas que mantienen esto seguro:
- Una declaración SQL por
execute()llamada. Ambas implementaciones nativas preparan solo la primera declaración de una cadena, por lo que"CREATE ...; CREATE ..."silenciosamente ejecuta una de ellas. - Cada versión se ejecuta en una transacción, por lo que un error a mitad de camino deja la base de datos en la versión anterior y el paso se re-ruta en la próxima ejecución.
- Preferir cambios adicionales (nuevas tablas, nuevas columnas nulas). SQLite admite
ALTER TABLE ... DROP COLUMNyRENAME COLUMNen versiones recientes, pero el sistema SQLite en versiones de Android más antiguas puede estar detrás, por lo que la forma portátil de rediseñar una tabla es crear una nueva tabla, copiar, eliminar la antigua y renombrar.
Migraciones y actualizaciones en vivo
Si envías cambios de JavaScript con Capgo actualizaciones en vivo, tu migración code se envía con el paquete. Esto es conveniente, ya que un cambio de esquema no necesita una versión de almacenamiento. También significa que un rollback puede colocar un paquete más antiguo encima de un esquema más nuevo. Mantén las migraciones compatibles hacia atrás (añade columnas, no renombra ni elimina en la misma versión que deja de usarlas) y un paquete más antiguo sigue funcionando.
Leer y escribir datos
SQLConnection te da cuatro métodos que usarás todo el tiempo:
| Método | Devuelve | Usa para |
|---|---|---|
query(sql, params) |
SQLRow[] |
SELECT y PRAGMA lecturas |
run(sql, params) |
{ rowsAffected, insertId } |
INSERTAR, ACTUALIZAR, ELIMINAR |
execute(sql, params) |
{ rows, rowsAffected, insertId } |
Cualquier cosa, resultado completo |
executeBatch(ops) |
SQLResult[] |
Muchas declaraciones en un viaje de ida y vuelta |
Siempre vincula valores con ? reemplazos. Nunca construye SQL con concatenación de cadenas desde la entrada del usuario.
// 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;
}
Las filas regresan como objetos simples claveado por el nombre de columna, por lo que no hay un paso de mapeo de columnas/valores. SQLite no tiene un tipo booleano, por lo que almacena 1 y 0 y convertir en la capa de datos de la orilla. setPinned does.
Operaciones de transacción y escrituras en lote
Wrap every multi-step write in a transaction. It keeps data consistent and it is much faster, because SQLite syncs to disk once per commit instead of once 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() comités cuando la llamada a la función resuelve y se deshace si lanza una excepción. Si necesitas control manual también hay beginTransaction(), commit() y rollback() en la conexión.
Todas las llamadas comparten una sola conexión nativa por base de datos. Mientras una transacción está abierta, otros code que escriben en la misma base de datos se ejecutan dentro de ella. Mantenga las transacciones cortas y evite esperar solicitudes de red dentro de ellas.
Almacenar datos binarios
Pasa un Uint8Array y obtienes un Uint8Array El plugin codifica en base64 para su transporte y almacena un BLOB real.
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;
Mantenga los grandes medios (fotos, videos) como archivos y almacene solo la ruta en SQLite. Los BLOBs son adecuados para miniaturas, pequeñas adjuntas y embeddings.
Cifrando la base de datos
SQL rápido admite SQLCipher en iOS y Android. Es de opción, por lo que las aplicaciones que no lo necesitan no lo envían:
const db = await FastSQL.connect({
database: 'vault',
encrypted: true,
encryptionKey: keyFromSecureStorage,
});
En Android, agrega net.zetetic:sqlcipher-android a tu archivo build.gradlede tu aplicación. En iOS, cambia el pod a la CapgoCapacitorFastSql/SQLCipher subespecífica. La generación de la clave, el almacenamiento en la caja de claves o el almacén de claves, cifrar una base de datos existente y rotar claves se cubren paso a paso en Cómo cifrar una base de datos SQLite en CapacitorNo está disponible la cifrado en la web.
Soporte web
En la web Fast SQL utiliza la versión oficial @sqlite.org/sqlite-wasm y almacena el archivo en el Sistema de Archivos Privados de Origen (OPFS). Es un archivo SQLite real, no una copia almacenada en memoria y escrita en IndexedDB.
Su servidor de desarrollo y su host de producción deben enviar dos encabezados.
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Con 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'],
},
});
Si los encabezados faltan, el plugin registra un aviso y se desploma a una base de datos no persistente: todo funciona hasta que se recarga. CapgoCapacitorFastSql.configureWeb({ useOpfs, worker }) te permite cambiar los valores por defecto antes de la primera connect(), y es un no-op en nativo.
require-corp también bloquea imágenes y scripts de origen cruzado que no envían encabezados CORP o CORS, por lo que pruebe los embutidos de terceros cuando lo active.
Donde vive el archivo de la base de datos
| Plataforma | Ubicación | Notas |
|---|---|---|
| iOS | Documents/<name>.db |
Incluido por defecto en respaldos de iCloud y dispositivos |
| Androide | files/<name>.db en el almacenamiento interno de la aplicación |
Sujeto a las reglas de respaldo de Android Auto |
| Web | OPFS para el origen de la página | Se elimina si el usuario elimina los datos del sitio |
Dos consecuencias prácticas. En primer lugar, desinstalar la aplicación elimina el archivo en ambas plataformas móviles. En segundo lugar, los respaldos pueden restaurar el archivo de la base de datos en un nuevo dispositivo. Eso es lo que normalmente quieres, pero si la base de datos está cifrada con una clave que no viaja con el respaldo, el archivo restaurado no puede ser abierto. La guía de cifrado cubre cómo manejar eso.
Lista de verificación de rendimiento
- Habilitar
walMode. Los lectores detienen a los escritores. - Escribir en lote en una transacción con
executeBatch. Este es el mayor beneficio para las importaciones y sincronización. - Agregar índices para columnas que filtran o ordenan, luego verificar con
EXPLAIN QUERY PLAN. - Seleccionar solo las columnas que necesitas. Grandes
TEXTyBLOBcolumnas cuestan tiempo de transferencia incluso a través del canal HTTP. - Paginar con
WHERE created_at < ? ORDER BY created_at DESC LIMIT 50en lugar de grandesOFFSETvalores. - After deleting a lot of data, run
VACUUMocasionalmente para reducir el tamaño del archivo. Redescribe toda la base de datos, así que hazlo durante el tiempo de inactividad.
Solución de problemas
SQL execution failed en iOS justo después de conectar. La solicitud HTTP local fue bloqueada. Verifica que NSAllowsLocalNetworking esté en el lugar Info.plist y reconstruye la aplicación nativa, un recarga en vivo no es suficiente.
CLEARTEXT communication to localhost not permitted en Android. La configuración de seguridad de red está ausente o no se ha referenciado desde el manifiesto.
Solo se ejecutó la primera sentencia. Pasaste varias sentencias en una cadena. Divide las mismas, o utiliza executeBatch.
no such table después de una actualización. La migración no se ejecutó, o se ejecutó contra otro nombre de base de datos. Registre PRAGMA user_version en el arranque y compárela con tu array de pasos.
Un filtro en un valor booleano o computado nunca coincide en Android. Para consultas de lectura, la implementación de Android vincula parámetros como texto. Las comparaciones contra una columna con afinidad de entero aún funcionan porque SQLite convierte el texto, pero las comparaciones contra expresiones (por ejemplo json_extract(...) = ? o count(*) > ?) comparar texto con un número. 1/0 y utilice true/falsey usar CAST(? AS INTEGER) no devuelve filas en Android.
INSERT ... RETURNING no devuelve filas en Android. En la versión actual del plugin, Android solo devuelve filas para las declaraciones que comienzan con SELECT, PRAGMA o EXPLAIN. Utiliza insertId desde run() y selecciona la fila después. Lo mismo se aplica a las consultas que comienzan con WITHenvuelve ellos para que comiencen con SELECTo pruébalas en Android con anticipación.
Los datos desaparecen al recargar la página en el navegador. OPFS no está disponible, casi siempre porque los encabezados COOP/COEP faltan en el host desplegado.
Encryption is not available in this build en iOS. Has pasado encrypted: true sin cambiar a la subspec SQLCipher.
Going further
El SQL bruto está bien para aplicaciones pequeñas. Una vez que tienes más de unas pocas tablas, una capa tipada te ayuda:
- Kysely con Capacitor y SQLite: constructor de consultas tipo y con un pequeño controlador personalizado para Fast SQL.
- Drizzle ORM con Capacitor y SQLite: esquema en TypeScript, migraciones generadas.
- TypeORM con Capacitor y SQLite: decoradores y repositorios.
- Opciones de almacenamiento de valores clave en CapacitorCuando las preferencias o una tabla KV en SQLite son suficientes.
La referencia del complemento se encuentra en la documentación de Fast SQL y la página de plugins. Necesita una reconstrucción nativa de SQL cuando lo agregas o cambias su configuración. Si no deseas mantener un Mac para eso, Capgo Construye puedes producir binarios iOS y Android firmados en la nube. Dado que a partir de abril de 2026, las subidas a la Tienda de Mac App deben ser construidas con Xcode 26 o posterior, así que verifica tu máquina de construcción antes de la próxima versión.