Jazz y LiveStore: la base que se lleva puesta y el registro que la reconstruye
Jazz reparte una base de datos con permisos por fila entre dispositivo, borde y núcleo global, mientras LiveStore invierte la relación y hace del registro de eventos la única verdad de la que se deriva un SQLite reactivo.
Los tres motores anteriores comparten una premisa que casi nadie enuncia: existe una base de datos de servidor que ya era la verdad antes de que apareciera el cliente, y el trabajo del motor consiste en acercar trozos de esa verdad al dispositivo. Los dos de esta lección rompen esa premisa por lados opuestos. Jazz reparte la base entera —con sus permisos, su identidad y su historia— entre el dispositivo del usuario y una jerarquía de servidores, de modo que el dato local no es una copia de nada sino el dato; su propia documentación lo dice sin rodeos, no hay diferencia entre estado optimista y estado real. LiveStore hace algo aún más radical: declara que la base de datos no es la verdad en ningún sitio, que la verdad es un registro ordenado de eventos y que la tabla es solo una lectura conveniente de ese registro, reconstruible siempre que haga falta. Uno responde a la pregunta de quién es dueño del dato y el otro a la de qué es el dato; ambos, si aciertas al elegirlos, te ahorran una capa entera de aplicación.
- Distinguir las dos generaciones de Jazz y no atribuir a una lo que documenta la otra.
- Entender la jerarquía local, borde y núcleo, y qué significa esperar en cada nivel.
- Modelar una aplicación como eventos y materializadores, y saber por qué deben ser deterministas.
- Reconocer los tres límites que la documentación de LiveStore declara para sí misma.
Antes de entrar, una advertencia sobre las fuentes que ahorra confusiones considerables: Jazz ha reescrito su producto y su documentación, y a día de hoy conviven dos manuales distintos con dos modelos de datos distintos bajo la misma marca. El manual clásico, alojado aparte, describe el sistema de valores colaborativos; el manual actual describe un motor relacional. Afirmar de uno lo que dice el otro es el error de lectura más frecuente que se comete con este proyecto.
Con LiveStore hay que tomar una precaución distinta, más de expectativas que de fuentes. Su documentación incluye una página dedicada al estado del proyecto y una sección de sostenibilidad con patrocinio, lo cual es una forma educada de decir que estás mirando una tecnología joven mantenida por un equipo pequeño. Nada de eso la descalifica; simplemente hay que leerla sabiendo que las respuestas honestas sobre madurez están escritas y a mano, en vez de suponerlas.
Jazz: la base de datos que se lleva puesta
En su formulación clásica, Jazz se presenta como una base de datos distribuida entre el navegador, los contenedores y las funciones, cuyos datos se manipulan como si fueran estado local reactivo. La unidad no es la fila sino el valor colaborativo: mapas, listas, cadenas de texto colaborativas, flujos de anexión por autor y flujos de fichero, cada uno con su identificador estable y su historia de ediciones. Sobre esa base, el manual clásico enumera lo que trae puesto sin que tengas que montarlo: autenticación, organizaciones y equipos, multijugador en tiempo real, historial de ediciones, permisos, cifrado de extremo a extremo y funcionamiento sin conexión. Esa lista es exactamente lo que significa la expresión baterías incluidas, y es también la razón por la que la documentación de otros motores remite a Jazz cuando alguien pide una solución completa en lugar de una pieza.
La generación actual cambia la superficie sin cambiar la tesis. Ahora el manual habla de tablas definidas en TypeScript, de consultas con filtros y relaciones, y de permisos a nivel de fila; lo que se conserva es la afirmación central de que el cliente lleva una réplica de la base y de que la escritura local es el estado, no una promesa sobre él. El motor mantiene por cada fila lógica una identidad estable, un estado visible que es lo que leen las consultas y un historial de versiones de fila; ante escrituras concurrentes sobre el mismo campo gana la más reciente, y las versiones perdedoras no se tiran, se conservan en la historia.
// Definir el esquema y escribir: la escritura local ya es el estado
const schema = {
todos: s.table({ title: s.string(), done: s.boolean() }),
};
const app = s.defineApp(schema);
db.insert(app.todos, { title: "Enviarlo", done: false });
// Permisos por fila, declarados aparte del codigo de la interfaz
export default definePermissions(app, ({ policy, allOf, session }) => {
const esInvitado = session.where({ authMode: "local-first" });
policy.todos.allowRead.always();
policy.todos.allowInsert.where(allOf([{ not: esInvitado }]));
});
El mecanismo que gobierna qué llega al dispositivo son las suscripciones a consultas, y merece detenerse porque se parece a la sincronización dirigida por consulta de la primera lección solo en la superficie. Cuando la aplicación se suscribe, la consulta viaja hacia arriba; el servidor la evalúa contra su estado relacional, devuelve las filas que casan y recuerda la suscripción, manteniendo un grafo de consultas vivo. Cuando algo cambia, no recalcula todo ni reenvía una fotografía: reasienta solo la parte afectada y empuja los incrementos. De ahí sale la propiedad que la documentación destaca con razón, que solo hay un momento de carga en la vida de una consulta, el primero; a partir de ahí los datos están en local y se leen aunque no haya red.
La sincronización se organiza en tres niveles: el local, que en el navegador vive en un trabajador dedicado sobre el sistema de ficheros privado del origen y coordina las pestañas eligiendo una como líder y reencaminando por ella a las demás; el borde, que es el primer salto de servidor y guarda lo necesario para servir las suscripciones que pasan por él; y el núcleo global, donde todo acaba reconciliándose. Las escrituras suben, las lecturas bajan bajo demanda, y cada nivel puede confirmar que ha recibido el dato de forma duradera. Sobre esa confirmación se construyen los niveles de durabilidad, que son la pieza más original del diseño: al escribir eliges hasta dónde quieres esperar, y esperar más solo tiene sentido si te importa que el dato haya salido del dispositivo o que sea visible entre regiones.
El modo de autenticación que el manual llama local-first genera un secreto en el cliente, deriva de él una cuenta estable y firma sus propios testigos, de modo que el servidor los verifica sin necesitar ningún almacén externo de claves ni proveedor. El usuario empieza a trabajar sin registrarse y el secreto es la cuenta. La documentación es igual de clara con la contrapartida: si el secreto se pierde, la cuenta y sus datos se pierden. Ofrece dos rescates, una frase de veinticuatro palabras que codifica el mismo secreto —sin cifrado adicional: quien la lea entra— y un respaldo en llave de acceso que guarda el secreto dentro de una credencial del navegador, con las fronteras de plataforma que eso arrastra. Y ofrece la salida elegante: probar sin cuenta, y más tarde subir a un proveedor externo conservando la identidad mediante una prueba de propiedad de corta vida que el servidor de registro verifica.
Sobre esa reconciliación por campo, el manual añade una advertencia que cualquiera que haya operado un sistema convergente reconoce al instante y que muy pocos proyectos se molestan en escribir: el motor resuelve el conflicto estructural, no el semántico. Si una persona renombra una tarea mientras otra la marca como terminada, ambos cambios se conservan porque tocan campos distintos, y el resultado puede ser una tarea completada cuyo nuevo título describe algo que nadie hizo. Ninguna regla de fusión automática puede detectar eso, porque el significado no está en los datos; sigue haciendo falta juicio de aplicación sobre qué ediciones simultáneas tienen sentido permitir.
Queda una observación de prudencia que ninguna de las dos documentaciones esconde y que hay que pesar antes de comprometer un producto. La reescritura es reciente: el manual actual instala el servidor autoalojado con una etiqueta de versión temprana y el panel de control vive en un subdominio nuevo, mientras el manual clásico sigue en pie con su propio modelo. Que un proyecto rehaga su modelo de datos entero es señal de ambición y también de que la superficie que aprendas hoy puede no ser la de dentro de dos años; es exactamente el tipo de dato que la última lección de este nivel convierte en un eje de decisión y no en una nota al pie.
LiveStore: el registro es la verdad y la tabla es una opinión
LiveStore parte de la separación entre modelo de escritura y modelo de lectura, que es el patrón de origen de eventos de toda la vida llevado hasta el cliente. El modelo de escritura es un registro ordenado e inmutable de eventos con nombres de dominio: cita agendada, cita reprogramada, participante invitado. El modelo de lectura es una base SQLite que no se escribe nunca directamente, sino que se materializa aplicando esos eventos en orden. La documentación lo resume comparándolo con un patrón conocido: es como el reductor de una aplicación de flujo unidireccional, pero persistido y sincronizado entre dispositivos.
// El evento describe la intencion; el materializador la proyecta sobre la tabla
export const events = {
todoCreated: Events.synced({
name: "v1.TodoCreated",
schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
}),
};
export const materializers = State.SQLite.materializers(events, {
[events.todoCreated.name]: defineMaterializer(
events.todoCreated,
({ id, text }) => todos.insert({ id, text }),
),
});
// El identificador se genera fuera y viaja dentro del evento
store.commit(events.todoCreated({ id: nanoid(), text: "Comprar pan" }));
La sincronización está declaradamente inspirada en el control de versiones. El cliente debe traerse los eventos remotos antes de poder empujar los suyos, y los eventos locales pendientes se rebasan sobre los recién llegados antes de enviarse; el servicio de sincronización no interpreta nada, se limita a almacenar eventos, imponer un orden total y avisar de las novedades. Ante conflictos concurrentes el comportamiento por omisión es de última escritura ganadora, con la posibilidad de escribir tu propia lógica de fusión, que aquí es más fácil que en otros motores por una razón estructural: tienes delante la intención del usuario y no solo el valor resultante.
Que el servicio de sincronización no interprete los eventos tiene una consecuencia agradable: el servidor puede ser un cliente más. La documentación recoge ese patrón, el de procesos de servidor que se suscriben al mismo registro para reaccionar a lo que pasa, y con él resuelve de forma natural aquello que en el modelo de Zero exigía cuidado especial —enviar correos, llamar a servicios externos— sin meter efectos secundarios dentro de una transacción.
Del lado del almacenamiento local la documentación describe una combinación deliberada: un SQLite en memoria para que las consultas bajen del milisegundo y un SQLite persistente para que el estado sobreviva entre sesiones. Y del lado de la infraestructura, el proyecto separa con limpieza dos cosas que otros motores mezclan: los adaptadores de plataforma —web, Expo para móvil, Node, Electron, Tauri y objetos duraderos de Cloudflare— y los proveedores de sincronización, donde la documentación oficial enumera una implementación sobre trabajadores de Cloudflare, otra sobre ElectricSQL y otra sobre S2, además de explicar cómo escribir la tuya. Que el segundo motor de este nivel aparezca como transporte del quinto es la mejor ilustración posible del argumento que cerraba aquella lección: las piezas estrechas se componen.
Determinismo: la regla que sostiene el edificio
Si un materializador no es determinista, todo el argumento se cae, y la documentación lo dice con todas las letras: los materializadores deben ser deterministas y sin efectos secundarios, y todo lo que necesiten tiene que llegar dentro de la carga del evento. El ejemplo canónico del manual es el que más se equivoca en la práctica: generar el identificador dentro del materializador produce uno distinto en cada dispositivo que reproduce el mismo evento, y con ello dos bases divergentes a partir de un registro idéntico. La solución es la misma que ya apareció en la primera lección de este nivel por un motivo distinto —generar el identificador fuera y pasarlo como dato—, y esa coincidencia no es casual: cualquier sistema que ejecute la misma operación más de una vez acaba en la misma regla.
flowchart TD A[intencion del usuario] --> B[evento con carga completa] B --> C[registro local inmutable y ordenado] C --> D[materializador determinista en transaccion] D --> E[sqlite reactivo] E --> F[interfaz] C -->|empujar tras traer y rebasar| G[servicio de sincronizacion] G -->|orden total| C style C fill:#f9e2af,color:#11111b style D fill:#a6e3a1,color:#11111b style G fill:#89b4fa,color:#11111b
El manual permite además leer el estado actual desde dentro de un materializador, y esa concesión merece una lectura atenta porque es donde se cuela el error sutil. Consultar la base durante la materialización sigue siendo determinista —la base en ese punto es función exclusiva de los eventos anteriores— siempre que no mezcles en el resultado nada que venga de fuera del registro. En cuanto un materializador mira el reloj, el generador de números aleatorios o una variable de entorno, el determinismo se rompe y dos dispositivos que han visto los mismos eventos dejan de coincidir sin que nada falle de forma visible.
El resto de propiedades se derivan de ahí. Cada materializador corre dentro de una transacción, de modo que si falla se deshacen sus escrituras y el evento queda marcado como problemático en lugar de dejar la base a medias. El modelo de lectura puede cambiar de forma sin migraciones de datos, porque no hay datos que migrar: cambias las tablas y los materializadores, se reproduce el registro y aparece la base nueva. Y la auditoría deja de ser una funcionalidad que se añade: el registro es la auditoría, motivo por el cual el manual señala a los agentes autónomos que necesitan estado local duradero y revisable como uno de sus casos fuertes.
Los datos del cliente deben caber en un SQLite en memoria; la documentación considera razonables bases de hasta un gigabyte según el dispositivo y, por encima, recomienda segmentar en varias bases por proyecto o espacio de trabajo. La sincronización está pensada para concurrencia pequeña o media —del orden de decenas o pocos cientos de personas colaborando sobre el mismo registro— y la vía de escalado es horizontal, repartiendo el trabajo entre registros distintos. Y el registro crece: la propia sección sobre origen de eventos enumera como desventaja la necesidad de vigilar que no se desmande. Hay una cuarta advertencia menor pero honesta, y es que empaquetar SQLite añade unos cuantos cientos de kilobytes al paquete de la aplicación.
Qué clase de aplicación pide cada uno
Antes de teorizar conviene hacer el ejercicio que el propio manual de LiveStore propone y que cuesta diez minutos: escribe los eventos de tu aplicación en pasado, como hechos ya ocurridos, y las tablas que se derivarían de ellos. Si la lista sale sola —cita agendada, cita reprogramada, invitación respondida— el modelo te encaja y lo vas a agradecer. Si te ves inventando eventos que se llaman actualizar fila o guardar formulario, la respuesta ya la tienes: estás describiendo mutaciones, no intenciones, y el origen de eventos solo te va a cobrar el peaje sin darte el beneficio.
La respuesta más útil no la doy yo: la da la documentación de LiveStore, en una sección que enumera cuándo no usarlo y que remite por nombre a los otros motores. Si ya tienes una base de datos que es la fuente de verdad, dice, usa Zero o ElectricSQL. Si quieres una solución completa con autenticación y almacenamiento incluidos, dice, mira Jazz o Instant. Si tus datos están muy entrelazados entre usuarios —una red social, un mercado— o el reparto entre modelo de lectura y de escritura no te encaja, tampoco es para ti. Y añade una advertencia de madurez profesional poco común: si estás empezando, esta tecnología te va a costar, porque sus concesiones solo se entienden después de haber sufrido los problemas que resuelve.
Conviene mirar esta lección desde el lado del código que dejas de escribir, porque es donde se ve el retorno. La tabla comparativa del manual de Jazz lo enumera sin adornos frente a la aplicación tradicional: el camino de lectura deja de ser una petición con su espera, el de escritura deja de ser una petición con su confirmación, el estado optimista deja de implementarse porque la escritura local ya es autoritativa y por tanto no hay reversión que programar, el soporte sin conexión deja de ser una cola artesanal con sus reintentos, y los estados de carga se reducen a la primera conexión. En LiveStore la lista es distinta pero igual de larga: desaparece la lógica de invalidación de caché, desaparecen las migraciones del modelo de lectura y desaparece el trabajo de instrumentar la auditoría. Al comparar motores es fácil contar solo lo que hay que aprender; contar también lo que se borra cambia bastante el resultado.
Jazz cuando el usuario es el dueño
Datos personales o de grupos pequeños, permisos por fila declarados, identidad que puede vivir en el dispositivo y aplicación que quieres tener funcionando sin montar un backend propio.
LiveStore cuando la intención importa
Herramientas de productividad, aplicaciones con deshacer y rehacer, registros de auditoría, agentes autónomos; todo aquello donde saber qué quiso hacer el usuario vale más que saber cómo quedó la tabla.
Jazz reparte, no centraliza
Local, borde y núcleo global, con niveles de durabilidad para elegir cuánto esperas. En el navegador, trabajador dedicado sobre almacenamiento privado del origen y elección de pestaña líder.
LiveStore reconstruye
Cambiar el modelo de lectura no exige migrar nada: se reescriben los materializadores y se reproduce el registro. A cambio, el registro hay que vigilarlo y acotarlo.
Vale la pena observar además qué aparece en el índice de patrones de cada proyecto, porque un índice de patrones es una radiografía de lo que la arquitectura regala y de lo que cobra. LiveStore documenta como patrones el deshacer y rehacer, el control de versiones, las máquinas de estados y los efectos secundarios asociados a eventos: son cosas que en cualquier otra arquitectura se construyen y que aquí se derivan de tener el registro. Jazz, por su parte, documenta como recetas los modelos de acceso —datos propiedad del usuario, permisos de grupo, acceso compartido, herencia en jerarquías—, que es justamente lo que cuesta caro cuando no viene resuelto. Cada uno regala aquello sobre lo que decidió apostar.
Fíjate en que esa lista de exclusiones dibuja, sin proponérselo, el eje que ninguna comparativa de rendimiento captura: el de si la aplicación se organiza alrededor de un dato compartido por mucha gente o alrededor de un dato que pertenece a alguien. Los dos motores de esta lección viven en el segundo extremo. Y esa pertenencia no es un valor ético abstracto, es una decisión arquitectónica con consecuencias medibles en cuánto código escribes y en qué pasa cuando el servidor no está.
Hay una simetría final que conviene llevarse porque ordena la lección siguiente. Jazz te pide confiar en su modelo de datos y te devuelve casi todo lo demás resuelto; LiveStore te pide un ejercicio de diseño de dominio que no todo el mundo sabe hacer y te devuelve una capacidad que ningún otro motor de este nivel ofrece, la de cambiar de opinión sobre cómo guardas los datos sin perder ni uno. Ninguno de los dos se enchufa a la base de datos que ya tienes, y esa es la primera pregunta del árbol de decisión que viene ahora.
- Elige un dominio pequeño y real y escribe su esquema de tablas para Jazz, con al menos una política de lectura y otra de escritura.
- Escribe el mismo dominio como una lista de eventos con nombres en pasado, y comprueba si alguno de ellos necesita datos que la interfaz no tiene a mano.
- Implementa los materializadores correspondientes y verifica que ninguno genera identificadores ni consulta el reloj.
- En Jazz, escribe con el nivel de durabilidad local y luego con el de borde, y mide la diferencia percibida en la interfaz.
- En LiveStore, cambia una tabla del modelo de lectura y reproduce el registro para comprobar que no pierdes nada.
- Escribe medio folio decidiendo cuál de los dos usarías para tu aplicación, citando dos límites declarados de cada uno.