Saltar al contenido principal

Almacenamiento clave-valor en Capacitor: Preferences vs SQLite

Compara el almacenamiento key-value en Capacitor: Preferences, almacenes SQLite key-value y Keychain. Descubre cuál encaja con ajustes, caché y secretos.

Logotipo de Capgo para una comparación de opciones de almacenamiento clave-valor en Capacitor

Para el almacenamiento clave-valor en Capacitor, usa @capacitor/preferences para unos pocos ajustes pequeños, un almacén clave-valor basado en SQLite para lo que sea más grande o numeroso, y Keychain o Keystore para secretos. Esta guía compara las opciones disponibles en Capacitor 8, muestra code funcional para cada una y explica cómo migrar de una a otra sin perder datos del usuario.

La mayoría de las apps usan dos de estas opciones a la vez: Preferences o SQLite para el estado de la app, y almacenamiento seguro para el token de autenticación.

Las opciones de un vistazo

localStorage / IndexedDB @capacitor/preferences SQL rápido KeyValueStore @capgo/capacitor-data-storage-sqlite @capgo/capacitor-native-biometric setData
Respaldado por Almacenamiento de WebView UserDefaults / SharedPreferences Archivo SQLite Archivo SQLite Keychain / Keystore
Resiste la limpieza de almacenamiento del sistema No garantizado Sí Sí Sí Sí
Tipos de valor Cadenas (localStorage) Cadenas Valores JSON, números, booleanos, Uint8Array Cadenas Cadenas
Rango de tamaño ideal Pequeño Pequeños, unos pocos KB por valor Valores grandes, muchas claves Muchas claves Muy pequeños (menos de ~8 KB en Android)
Cifrado No No SQLCipher opcional, clave desde almacenamiento seguro SQLCipher opcional, contraseña en la configuración Sí, respaldado por hardware
Namespaces Por origen group opción store opción Tablas Nombres de clave
Consultar por prefijo No No Listar claves filtervalues No
Web Sí localStorage SQLite Wasm (OPFS) IndexedDB No apto para secretos

¿Por qué no localStorage?

localStorage e IndexedDB funcionan dentro de una Capacitor WebView, pero los datos pertenecen a la WebView, no al almacenamiento nativo de tu app. El SO puede recuperar el almacenamiento de la WebView cuando al dispositivo le queda poco espacio, y la propia documentación de Capacitor recomienda no depender de él para datos que no puedes perder. Cambiar el hostname o el scheme de la app en capacitor.config también cambia el origen, lo que apunta la WebView a un almacenamiento distinto y vacío.

Úsalos como una caché que puede desaparecer. Para todo lo demás, usa una de las opciones con respaldo nativo.

Opción 1: Capacitor Preferences para ajustes pequeños

@capacitor/preferences es el plugin oficial para datos clave-valor ligeros. En iOS escribe en UserDefaults, en Android en SharedPreferences, en la web en localStorage.

bun add @capacitor/preferences
bunx cap sync
import { Preferences } from '@capacitor/preferences';

interface Settings {
  theme: 'light' | 'dark' | 'system';
  fontScale: number;
}

const DEFAULTS: Settings = { theme: 'system', fontScale: 1 };

export async function loadSettings(): Promise<Settings> {
  const { value } = await Preferences.get({ key: 'settings' });
  return value ? { ...DEFAULTS, ...JSON.parse(value) } : DEFAULTS;
}

export async function saveSettings(settings: Settings): Promise<void> {
  await Preferences.set({ key: 'settings', value: JSON.stringify(settings) });
}

Úsalo para: tema, idioma, flags de onboarding, la última pestaña seleccionada, feature toggles.

No lo uses para:

  • Secretos. Los valores se almacenan en texto plano dentro del sandbox de la app y se incluyen en las copias de seguridad del dispositivo.
  • Valores grandes. UserDefaults y SharedPreferences cargan su archivo completo en memoria. Los blobs JSON grandes ralentizan cada lectura y escritura.
  • Muchas claves que necesitas listar o borrar por prefijo. keys() devuelve todo, así que filtras en JavaScript.

Preferences.configure({ group: 'MyApp' }) cambia el namespace bajo el que se almacenan las claves. El grupo predeterminado es CapacitorStorage.

Opción 2: un almacén de clave-valor en SQLite

Cuando ya guardas respuestas en caché de API, borradores, colas offline o cientos de entradas, un archivo SQLite es mejor opción. Soporta valores grandes, las escrituras son atómicas y puedes cifrarlo.

KeyValueStore SQL rápido

@capgo/capacitor-fast-sql incluye una KeyValueStore clase sobre su conexión SQLite. Crea una __kv_store tabla, mantiene el tipo de valor y soporta múltiples stores nombrados en una sola base de datos.

bun add @capgo/capacitor-fast-sql
bunx cap sync

Fast SQL requiere dos ajustes nativos (red local en iOS en Info.plist, una regla de texto plano en localhost en Android). Se explican en Cómo usar SQLite en una app de Capacitor.

import { KeyValueStore } from '@capgo/capacitor-fast-sql';

const cache = await KeyValueStore.open({ database: 'app', store: 'api-cache' });

// Objects, arrays, numbers, booleans, null and Uint8Array are stored as-is
await cache.set('user:42', { id: 42, name: 'Ada', roles: ['admin'] });
await cache.set('lastSync', Date.now());

const user = (await cache.get('user:42')) as { id: number; name: string } | null;
const exists = await cache.has('lastSync');
const allKeys = await cache.keys();

await cache.remove('user:42');
await cache.clear(); // only clears the 'api-cache' store
await cache.close();

Resumen de API: open(options), fromConnection(connection, store), set, get (devuelve null cuando falte), has, remove, clear, keys, close.

Dos patrones que conviene conocer:

Varios stores, una sola base de datos. El store opción de namespaces para las keys, así settings, drafts y api-cache pueden coexistir y borrarse de forma independiente.

Key-value junto a tablas reales. Si tu app ya usa Fast SQL para datos relacionales, reutiliza la conexión en lugar de abrir una segunda:

import { FastSQL, KeyValueStore } from '@capgo/capacitor-fast-sql';

const db = await FastSQL.connect({ database: 'app' });
const drafts = await KeyValueStore.fromConnection(db, 'drafts');
// You own `db`: drafts.close() does not disconnect it

Más adelante, cuando un blob key-value crezca hasta convertirse en algo que quieras consultar (por ejemplo, todos los borradores de hace más de una semana), puedes moverlo a una tabla real en la misma base de datos.

Almacenamiento key-value cifrado

KeyValueStore.open acepta las mismas opciones que FastSQL.connect, incluyendo encrypted y encryptionKey en iOS y Android:

import { KeyValueStore } from '@capgo/capacitor-fast-sql';
import { NativeBiometric } from '@capgo/capacitor-native-biometric';

async function getKey(): Promise<string> {
  const { isSaved } = await NativeBiometric.isDataSaved({ key: 'kv.key' });
  if (isSaved) return (await NativeBiometric.getData({ key: 'kv.key' })).value;
  const bytes = crypto.getRandomValues(new Uint8Array(32));
  const key = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
  await NativeBiometric.setData({ key: 'kv.key', value: key });
  return key;
}

const secureStore = await KeyValueStore.open({
  database: 'secure-kv',
  encrypted: true,
  encryptionKey: await getKey(),
});

Esto requiere SQLCipher en los builds nativos. La configuración, y qué hacer si se pierde la clave tras restaurar un backup, está en Cómo cifrar una base de datos SQLite en Capacitor.

@capgo/capacitor-data-storage-sqlite

@capgo/capacitor-data-storage-sqlite es un plugin de clave-valor dedicado sobre SQLite (originalmente de Jean Pierre Quéau, ahora mantenido por Capgo). Los valores son strings, y añade algunos extras: múltiples tablas por store, filtrado por prefijo y sufijo, e importación y exportación JSON.

bun add @capgo/capacitor-data-storage-sqlite
bunx cap sync
import { CapgoCapacitorDataStorageSqlite as Store } from '@capgo/capacitor-data-storage-sqlite';

await Store.openStore({ database: 'appStore', table: 'settings' });

await Store.set({ key: 'locale', value: 'fr' });
await Store.set({ key: 'profile', value: JSON.stringify({ name: 'Ada' }) });

const { value } = await Store.get({ key: 'locale' });
const { result: hasProfile } = await Store.iskey({ key: 'profile' });
const { keys } = await Store.keys();

// Switch table inside the same store
await Store.setTable({ table: 'drafts' });
const { values } = await Store.filtervalues({ filter: 'note%' }); // values whose key starts with "note"

await Store.remove({ key: 'note-1' });
await Store.clear(); // clears the current table

filtervalues aplica el filtro como un LIKE patrón SQL en la clave (note% para un prefijo, %note para un sufijo, texto plano para contiene). El patrón se inserta en la cadena SQL, así que solo pasa filtros que controles, nunca input del usuario sin procesar.

En la web usa IndexedDB a través de localforage, que se instala por separado:

bun add localforage

Sobre su cifrado: openStore({ encrypted: true, mode: 'secret' }) usa SQLCipher con una contraseña de capacitor.config en plugins.CapgoCapacitorDataStorageSqlite.encryptionSecret. Esa contraseña se incluye en el bundle de tu app, y si no la configuras, el plugin usa un valor predeterminado integrado. Trátalo como protección contra inspecciones casuales de archivos, no contra alguien que descomprima tu app. Para secretos reales, usa una clave de Keychain o Keystore, como en el ejemplo de Fast SQL anterior.

¿Fast SQL KeyValueStore o data-storage-sqlite?

  • Elige SQL rápido KeyValueStore si quieres valores tipados sin JSON manual, valores binarios, una clave de cifrado en tiempo de ejecución, o ya usas Fast SQL para tablas.
  • Elige data-storage-sqlite si necesitas sus funciones de tablas y filtros, exportación e importación JSON, o soporte para Electron.

Opción 3: Keychain y Keystore para secretos

Auth tokens, refresh tokens, API keys and database encryption keys belong in platform secure storage. @capgo/capacitor-native-biometric (8.6.0 y versiones posteriores) expone una API de clave-valor genérica sobre el Keychain de iOS y el Keystore de Android:

bun add @capgo/capacitor-native-biometric
bunx cap sync
import { AccessControl, NativeBiometric } from '@capgo/capacitor-native-biometric';

// Silent read and write
await NativeBiometric.setData({ key: 'session.refreshToken', value: token });
const { value: refreshToken } = await NativeBiometric.getData({ key: 'session.refreshToken' });

// Require Face ID / Touch ID / fingerprint to read
await NativeBiometric.setData({
  key: 'wallet.pin',
  value: pin,
  accessControl: AccessControl.BIOMETRY_ANY,
});
const { value: secretPin } = await NativeBiometric.getSecureData({
  key: 'wallet.pin',
  reason: 'Confirm it is you',
});

await NativeBiometric.deleteData({ key: 'session.refreshToken' });

Mantén los valores pequeños. El plugin documenta que el cifrado respaldado por Android Keystore funciona mejor con menos de 8 KB por valor. Si necesitas proteger muchos datos, guarda una clave aleatoria aquí y úsala para cifrar una base de datos SQLite. Nuestro artículo sobre almacenamiento seguro para tokens sin conexión profundiza en el manejo de tokens.

Recuerda que los elementos del Keychain en iOS pueden sobrevivir a la desinstalación de la app. En el primer arranque tras una instalación limpia, borra los secretos obsoletos si tu app espera empezar desde cero (guarda un flag de «primera ejecución completada» en Preferences, que se elimina al desinstalar, y borra las entradas del Keychain cuando no esté).

Cómo elegir: lista de decisiones

  1. ¿Es un dato sensible? Token, contraseña, PIN o clave. Usa Keychain o Keystore.
  2. ¿Son solo unos pocos ajustes pequeños? Usa Preferences.
  3. ¿Son muchas entradas, un JSON grande, datos binarios o un caché que quieras expirar y limpiar por grupos? Usa un almacén key-value en SQLite.
  4. ¿Necesitas filtrar, ordenar o hacer joins? Ya no son datos key-value. Crea una tabla, consulta la guía de SQLite, o usa una capa tipada como Kysely.

Migrar datos existentes entre almacenes

Pasar de localStorage o Preferences a un almacén de SQLite es una copia única al inicio. Hazlo antes de que el resto de la app lea datos y registra que se ejecutó.

import { Preferences } from '@capacitor/preferences';
import { KeyValueStore, type KeyValueValue } from '@capgo/capacitor-fast-sql';

export async function migrateToSqliteStore(): Promise<KeyValueStore> {
  const kv = await KeyValueStore.open({ database: 'app', store: 'settings' });
  if (await kv.has('__migrated_v1')) return kv;

  const { keys } = await Preferences.keys();
  for (const key of keys) {
    const { value } = await Preferences.get({ key });
    if (value === null) continue;
    let parsed: KeyValueValue = value;
    try {
      parsed = JSON.parse(value) as KeyValueValue;
    } catch {
      // keep plain strings as strings
    }
    await kv.set(key, parsed);
  }

  // Also copy from localStorage if older versions used it
  for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    if (key && !(await kv.has(key))) {
      await kv.set(key, localStorage.getItem(key));
    }
  }

  await kv.set('__migrated_v1', true);
  // Remove the old copies only after a release or two, once you are confident
  return kv;
}

Elimina los secretos del almacén antiguo en cuanto se copien al almacenamiento seguro. Dejar un token en Preferences tras moverlo al Keychain mantiene la exposición original.

Como este code se ejecuta en JavaScript, puedes publicarlo con un Capgo live update. El plugin de SQLite es nativo, así que la release que lo incluye necesita un build de tienda. Publica primero el plugin y luego la migración code.

Solución de problemas

Los valores de Preferences son null después de una actualización en la web. El origen cambió (puerto, nombre de host o esquema), por lo que localStorage es un almacén diferente y vacío.

SQL rápido KeyValueStore.open lanza una excepción en iOS. El canal HTTP local está bloqueado. Añade NSAllowsLocalNetworking a Info.plist y recompila la app nativa.

get devuelve una cadena donde esperabas un objeto en data-storage-sqlite. Ese plugin solo almacena cadenas. JSON.parse el valor.

Secretos ausentes tras restaurar una copia de seguridad en un teléfono nuevo. Las claves de Keystore y los elementos de Keychain exclusivos del dispositivo no se transfieren a hardware nuevo. Trata el almacenamiento seguro como una caché de credenciales que puedes obtener de nuevo iniciando sesión.

Configuración perdida tras reinstalar, pero los tokens siguen ahí en iOS. Esperado: Preferences se elimina con la app, los elementos de Keychain pueden permanecer. Usa el patrón de flag de primera ejecución anterior.

Resumen

  • Preferences: ajustes pequeños y no sensibles.
  • Almacén key-value en SQLite (Fast SQL KeyValueStore o @capgo/capacitor-data-storage-sqlite): muchos o grandes valores, caché, borradores, opcionalmente cifrado.
  • Keychain o Keystore (@capgo/capacitor-native-biometric setData): secretos y claves de cifrado.

Referencias de plugins: Docs de Fast SQL, documentación de data-storage-sqlite, y la página del plugin de biometría nativa.

Live updates para apps Capacitor

Cuando un bug de la capa web está en producción, publica el fix a través de Capgo en lugar de esperar días a la aprobación de la app store. Los usuarios reciben la actualización en segundo plano mientras los cambios nativos siguen el flujo de revisión habitual.

Lo último de nuestro blog

Capgo te da la información clave que necesitas para crear una app móvil realmente profesional.