Por qué SQLite y no otra cosa
El motor embebido más desplegado del mundo aporta al cliente tres cosas que ningún almacén clave-valor ofrece: SQL completo, transacciones ACID reales y un formato de fichero con compromiso explícito de estabilidad durante décadas.
Llevas cinco niveles peleando con almacenes que guardan cosas: pares de clave y valor en la lección 6, objetos con índices y cursores en la 7 y la 8, ficheros opacos en la 9 y la 10, y una cuota que puede evaporarlo todo en la 11. Ninguno de ellos sabe responder preguntas. Cada consulta no trivial que has escrito hasta aquí ha sido, en realidad, un motor de base de datos improvisado en JavaScript: un recorrido manual, una unión hecha a mano, una agregación en un bucle. Lo que abre este nivel es la alternativa: meter en el navegador un motor de verdad, escrito hace veinticinco años en C, probado hasta la obsesión y desplegado en más dispositivos que ningún otro software de bases de datos del planeta. Antes de ver cómo llega hasta ahí, conviene entender exactamente qué es lo que llega.
- Distinguir un motor embebido de un servidor de bases de datos, y por qué esa distinción es la que lo hace apto para el cliente.
- Identificar las tres propiedades que SQLite aporta y que un almacén clave-valor no puede ofrecer sin reimplementarlas.
- Entender el compromiso de estabilidad del formato de fichero y su valor concreto para una arquitectura local-first.
- Anticipar qué se desplaza en tu diseño cuando la capacidad de consultar deja de ser exclusiva del servidor.
Un motor embebido, no un servidor en miniatura
La confusión más frecuente entre quien llega desde el mundo de PostgreSQL o MySQL es imaginar SQLite como una versión reducida de aquello: un servidor pequeño, con menos funciones, para casos modestos. No es eso. Es una categoría distinta. No hay proceso servidor, no hay socket, no hay protocolo de red, no hay autenticación ni usuarios, no hay administrador. Hay una biblioteca que enlazas contra tu programa y una función que abre un fichero. Toda la base de datos —esquema, datos, índices, metadatos— vive en ese fichero, y las consultas se ejecutan dentro de tu propio proceso, en tu propio hilo, sobre tu propia pila.
Esa arquitectura es exactamente la razón por la que este nivel existe. Un servidor no cabe en una pestaña: necesita un proceso independiente, un puerto y un ciclo de vida propio. Una biblioteca sí cabe, porque no pide nada del sistema operativo salvo la capacidad de leer y escribir bytes en algún sitio. Y esa exigencia mínima —leer y escribir bytes— es lo que la lección 3 convertirá en el punto de apoyo de todo el nivel.
flowchart TB A[Tu aplicacion] --> B[API de la biblioteca] B --> C[Tokenizador y analizador] C --> D[Generador de codigo] D --> E[Maquina virtual que ejecuta el bytecode] E --> F[Arbol B con claves ordenadas] F --> G[Pager transacciones y diario] G --> H[Capa VFS] H --> I[Bytes en algun almacenamiento] style H fill:#f9e2af,color:#11111b style I fill:#89b4fa,color:#11111b
Merece la pena mirar esa pila con atención porque explica el resto del nivel. Las tres capas de arriba traducen tu SQL a un programa de bytecode; la máquina virtual lo ejecuta; el árbol B ordena las claves; el pager convierte todo eso en lecturas y escrituras de páginas de tamaño fijo y garantiza la atomicidad mediante un diario. Solo la capa de abajo habla con el mundo exterior. Ocho capas de ingeniería densa, y una única superficie de contacto con el sistema. Ese diseño no se hizo pensando en el navegador —es de mucho antes—, pero resulta que es justo el diseño que permite llevarlo al navegador.
Conviene añadir un dato que suele leerse como propaganda y no lo es: la relación entre código de producción y código de prueba en SQLite es de varios órdenes de magnitud a favor del segundo, con cobertura de ramas exhaustiva y baterías que simulan fallos de disco, de memoria y de energía en cada punto del recorrido. Cuando eliges este motor no estás eligiendo una implementación de SQL; estás heredando dos décadas y media de casos límite ya encontrados por otros.
Dos matices más completan el cuadro y ambos importan para el navegador. El primero es el modelo de concurrencia: sin servidor no hay nadie que arbitre, así que el arbitraje ocurre sobre el propio fichero mediante una escalera de bloqueos, con la regla clásica de varios lectores simultáneos o un único escritor. En un sistema operativo esa escalera la sostienen los bloqueos del núcleo; en un navegador habrá que sostenerla con lo que haya, y la lección 4 te enseñará cuánto duele. El segundo es la licencia: el código está en dominio público, sin obligaciones de atribución ni cláusulas que revisar. Para una pieza que vas a incrustar en el cliente de tu producto y mantener durante años, esa ausencia de fricción legal vale más de lo que parece a primera vista.
El adjetivo embebido describe el modo de despliegue, no la capacidad. Este motor sostiene bases de datos de cientos de gigabytes, admite consultas con decenas de uniones, tiene funciones de ventana, expresiones de tabla comunes recursivas, tablas virtuales, búsqueda de texto completo y un planificador que reordena uniones según estadísticas. La limitación real frente a un servidor no está en el lenguaje sino en la concurrencia de escritura y en la ausencia de gestión de usuarios y de red. Dicho de otro modo: lo que le falta es exactamente lo que en el cliente no necesitas.
Lo que aporta SQL que un almacén clave-valor no puede
Ponlo en términos operativos y no de catálogo de funciones. Con un almacén de objetos, una pregunta como cuántos documentos de cada carpeta modificó cada usuario el mes pasado se resuelve así: abres un cursor, recorres registros, cruzas manualmente con otra colección, acumulas en un mapa y ordenas al final. Cada registro cruza la frontera de serialización, se materializa como objeto de JavaScript y se descarta. Con SQL escribes la pregunta y el motor decide cómo responderla, dentro de su propia memoria, sin materializar nada que no vaya en el resultado.
SELECT c.nombre, u.nombre AS autor, COUNT(*) AS ediciones
FROM documentos d
JOIN carpetas c ON c.id = d.carpeta_id
JOIN usuarios u ON u.id = d.autor_id
WHERE d.modificado_en >= :desde
GROUP BY c.id, u.id
HAVING COUNT(*) > 3
ORDER BY ediciones DESC
LIMIT 20;
La diferencia no es de comodidad sintáctica. Es que en la versión con cursores tú eres el planificador de consultas: tú decides el orden de las uniones, tú eliges qué índice recorrer, tú cargas con el coste de haberlo decidido mal. Y a diferencia de un planificador de verdad, tu decisión queda congelada en el código y no se reconsidera cuando los datos cambian de forma.
Compara el esqueleto de la versión artesanal, reducido a lo esencial y sin el manejo de errores que en la práctica lo duplicaría:
// La misma pregunta, resuelta a mano sobre un almacen de objetos
const carpetas = await todas("carpetas"); // se materializa entera
const usuarios = await todas("usuarios"); // tambien
const conteo = new Map();
for await (const d of recorrer("documentos", "porFecha", rangoDesde(desde))) {
const clave = d.carpeta_id + "|" + d.autor_id; // clave compuesta inventada
conteo.set(clave, (conteo.get(clave) ?? 0) + 1);
}
const filas = [...conteo]
.filter(([, n]) => n > 3)
.sort((a, b) => b[1] - a[1])
.slice(0, 20)
.map(([clave, n]) => componer(clave, n, carpetas, usuarios));
Cuenta lo que ha pasado ahí. Dos colecciones enteras materializadas en memoria porque no había forma de cruzarlas de otro modo. Una clave compuesta inventada porque el almacén no sabe agrupar por dos campos. Un filtro, una ordenación y un recorte que se aplican después de haber recorrido todo. Y una función de composición final que existe únicamente para deshacer la concatenación que tuvimos que inventar. Nada de eso es un problema del programador: es lo que ocurre cuando el almacén no sabe responder y el lenguaje de consulta lo pone tu bucle.
Un lenguaje declarativo
Describes el resultado, no el recorrido. Uniones, agregaciones, subconsultas, expresiones de tabla comunes y funciones de ventana son operaciones del motor, no bucles tuyos.
Transacciones ACID
Atomicidad y durabilidad garantizadas por un diario de reversión, con reglas de integridad declaradas en el esquema: claves foráneas, unicidad y restricciones de comprobación.
Índices de verdad
Índices compuestos, parciales, sobre expresiones y de cobertura, con estadísticas que el planificador consulta para elegir el plan. No un único índice por campo.
Un esquema explícito
El esquema vive con los datos, no en la cabeza del equipo. Se puede inspeccionar, versionar y migrar con instrucciones que el propio motor valida.
Nada de lo anterior descalifica lo aprendido en los niveles 7 y 8. Un almacén de objetos es la herramienta correcta cuando el acceso es por clave, el volumen es moderado y el modelo de datos no tiene relaciones que cruzar. Traer un motor de SQL cuesta cientos de kilobytes de descarga, un Worker, una capa de mensajes y una disciplina de migraciones. Ese coste se justifica cuando las preguntas que tu producto necesita hacer son relacionales, no cuando simplemente quieres una API más agradable.
El formato de fichero como contrato
Hay una propiedad de SQLite que se cita poco en contextos web y que en local-first es probablemente la más valiosa de todas: el formato del fichero de base de datos es estable y está documentado, y sus autores mantienen un compromiso público de compatibilidad hacia atrás con horizonte de décadas. Un fichero escrito hoy se abrirá con las versiones futuras del motor, y también con las herramientas de terceros que entienden el formato, muchas de las cuales no existían cuando se escribió el fichero.
Compara eso con lo que has tenido hasta ahora. El contenido de IndexedDB es un detalle interno del navegador: no hay un formato publicado, no hay compromiso de estabilidad, no puedes abrirlo con otra herramienta y no es portable entre navegadores del mismo usuario. Lo que la lección 4 del nivel 4 llamaba propiedad real de los datos exigía dos cosas: acceso al contenido y un formato que sobreviva al programa que lo escribió. Un almacén de objetos te da la primera con esfuerzo y no te da la segunda en absoluto.
Este marco cambia decisiones de producto. Si la base de datos local de tu aplicación es un fichero SQLite, exportarla es copiar bytes, hacer copia de seguridad es copiar bytes, y el usuario que abandone tu producto se lleva un artefacto que cualquier analista puede consultar con herramientas estándar. Instituciones de archivo lo reconocen precisamente por eso como formato admisible para conservación de conjuntos de datos a largo plazo. Es difícil exagerar cuánta ansiedad de bloqueo de proveedor desactiva esa sola propiedad.
Qué cambia al ponerlo en el cliente
La consecuencia estructural es una redistribución de capacidades. Hasta ahora, en tu arquitectura, la capacidad de responder preguntas arbitrarias vivía únicamente en el servidor, porque era el único sitio con un motor de consultas. El cliente sabía pedir y pintar. Al bajar un motor completo, esa asimetría desaparece: el cliente puede formular una consulta que nadie previó al diseñar la API, agregarla, ordenarla y paginarla sin pedir permiso ni esperar a nadie.
Eso desplaza tres cosas a la vez. Los endpoints dejan de ser el vocabulario de lo posible, porque ya no hace falta un endpoint por pregunta. La latencia de una consulta pasa de decenas o cientos de milisegundos de red a fracciones de milisegundo de memoria local. Y aparece un problema nuevo, que este nivel abrirá y el siguiente tendrá que cerrar: si el cliente tiene un esquema, ese esquema hay que versionarlo y migrarlo en máquinas ajenas, con todas las versiones conviviendo a la vez.
El vocabulario deja de ser finito
Sin un motor local, cada pregunta nueva exige un endpoint. Con él, el conjunto de preguntas que el cliente puede formular sobre sus propios datos es abierto y no cuesta ninguna coordinación entre equipos.
La latencia cambia de unidad
Se pasa de decenas o cientos de milisegundos de ida y vuelta a fracciones de milisegundo sobre páginas ya en memoria. No es una mejora porcentual: es un cambio de qué interacciones son concebibles.
Aparece un esquema en el cliente
Con esquema vienen migraciones, versiones y compatibilidad hacia atrás, ejecutadas en máquinas que no controlas y que pueden estar meses sin actualizarse.
La exportación se vuelve trivial
La copia local es un fichero de formato público. Exportar deja de ser un proyecto y pasa a ser una operación de una línea sobre bytes que el usuario puede abrir con herramientas ajenas.
Hay una cuarta consecuencia, más silenciosa, que conviene anticipar desde ya porque condiciona el diseño desde el primer día: la aparición de una frontera de proceso dentro de tu propia aplicación. Como verás en la lección 2 y ya adelantó el nivel 10, el motor acabará viviendo en un Worker, y eso convierte tu capa de datos en un servicio con protocolo propio. La ganancia de latencia es real, pero se cobra en una travesía de mensajes por consulta que hay que presupuestar y en una API que conviene diseñar gruesa desde el principio, no descomponerla más tarde a golpe de incidentes.
Merece la pena entender qué se está decidiendo realmente aquí, porque no es una elección de biblioteca sino de reparto de poder dentro de tu propia arquitectura. En una aplicación cliente-servidor convencional, el servidor no solo custodia los datos: custodia la capacidad de interrogarlos, y esa custodia es la que hace que cada pregunta nueva del negocio se convierta en un ticket, un endpoint, un despliegue y una espera. El cliente no es tonto porque no sepa hacer uniones; es tonto porque se decidió que no las hiciera, y toda la conversación sobre APIs REST bien diseñadas frente a GraphQL es, mirada desde cierta altura, una discusión sobre cuánto de esa capacidad de interrogación se le devuelve al cliente y a través de qué peaje. Bajar un motor de SQL completo al navegador no mejora ese peaje: lo elimina para el subconjunto de datos que el cliente ya tiene. Y esa es exactamente la razón por la que la decisión es más grave de lo que parece, porque a partir de ahí tu cliente ya no es un consumidor de una API sino un nodo con esquema propio, plan de consulta propio y estado durable propio, es decir, un participante de pleno derecho en un sistema distribuido. Los tres capítulos que la industria lleva escribiendo desde hace décadas sobre replicación, convergencia y evolución de esquemas dejan de ser problemas del equipo de infraestructura y se convierten en problemas tuyos, en el navegador de un desconocido, sin acceso a la máquina y sin poder forzar una actualización. Quien adopta SQLite en el cliente creyendo que ha ganado velocidad ha entendido la mitad barata; la mitad cara es que ha aceptado gobernar un esquema que ya no controla.
- Localiza en tu código las tres funciones más largas que recorren un almacén de objetos y anota, para cada una, cuántos registros se materializan y cuántos acaban en la respuesta.
- Escribe la consulta SQL equivalente a cada una de las tres y compara la longitud, la legibilidad y el número de decisiones de ejecución que has tomado tú frente a las que tomaría un planificador.
- Enumera las preguntas que tu producto no hace hoy en el cliente porque serían inviables con cursores. Esa lista es la justificación real del nivel.
- Estima el coste de adopción: descarga del binario, arranque en frío, capa de mensajes y disciplina de migraciones. Contrástalo con la lista anterior y decide honestamente si compensa.
- Escribe en un párrafo qué significaría para tu usuario que su copia local fuera un fichero estándar exportable. Guárdalo: es el argumento que tendrás que defender en la lección 5.