Motores de sincronización
Los CRDTs resuelven la fusión de documentos concurrentes, pero muchas aplicaciones no editan un documento sino que consultan una base de datos relacional; para ellas ha surgido una nueva categoría de infraestructura, el motor de sincronización, que mantiene una base de datos local en el dispositivo —normalmente SQLite embebido o un store en memoria— sincronizada de forma automática y bidireccional con la base de datos del servidor. Esta lección explica el patrón común que comparten cuatro motores dominantes y sus apuestas divergentes: Electric SQL sincroniza subconjuntos de tablas de Postgres definidos como shapes mediante replicación lógica; Zero convierte la propia consulta en la unidad de sincronización con su lenguaje ZQL y mantenimiento incremental de vistas; Jazz sube el nivel de abstracción a un framework local-first completo construido sobre CRDTs, con permisos y cifrado integrados; y PowerSync empareja un SQLite en el cliente con Postgres, MongoDB o MySQL gobernado por reglas de sincronización. El hilo conductor es la replicación parcial: como nunca cabe la base de datos entera en el cliente, todo motor necesita un modo de declarar qué subconjunto viaja, y esa declaración —shape, consulta o regla— es la decisión de diseño que los distingue.
En el nivel treinta y seis construiste a mano una cola de mutaciones offline: encolar la intención, aplicarla optimistamente, drenarla al reconectar, reconciliar. Funcionaba, pero era artesanía frágil, y al final de aquella lección insinuamos que en producción rara vez se escribe todo eso desde cero. Los motores de sincronización son la respuesta industrial a ese problema: infraestructura que generaliza tu cola artesanal hasta convertirla en una base de datos local que se mantiene sola al día con el servidor, en ambos sentidos, sin que tu aplicación vuelva a escribir un fetch. La idea es tan simple de enunciar como difícil de implementar: tu app consulta una base de datos que vive en el dispositivo —un SQLite embebido, un store reactivo— y el motor se encarga, en segundo plano, de replicar hacia arriba tus escrituras y hacia abajo los cambios del servidor. La red desaparece de tu código; en su lugar queda una consulta local que siempre está fresca. Cuatro motores se disputan hoy esta categoría naciente —Electric SQL, Zero, Jazz y PowerSync—, y aunque sus arquitecturas difieren, todos giran alrededor del mismo eje y tropiezan con el mismo muro: la base de datos entera nunca cabe en el cliente, así que todos necesitan una forma de decir qué parte sincronizar. Esta lección los recorre y te deja el mapa de esa categoría.
- Comprender el patrón común del motor de sincronización: base de datos local sincronizada automática y bidireccionalmente con el servidor.
- Distinguir las apuestas de Electric SQL, Zero, Jazz y PowerSync y qué optimiza cada una.
- Entender la replicación parcial como el problema central y cómo cada motor la declara: shape, consulta o regla de sincronización.
- Situar estos motores como la generalización industrial de la cola de mutaciones offline artesanal.
La idea común: una base de datos local que se sincroniza sola
El patrón que comparten los cuatro motores es una inversión del acceso a datos que ya reconoces de la primera lección del nivel. En la arquitectura tradicional tu app pide datos al servidor con fetch, recibe una respuesta, la cachea y la muestra; cada pantalla es una petición y cada petición un punto de espera y de fallo. En la arquitectura de un motor de sincronización tu app no pide nada por la red: consulta una base de datos local que ya tiene los datos, y el motor los pone y los mantiene ahí. La consulta es instantánea porque es local, funciona sin red porque los datos ya están, y se actualiza sola porque el motor propaga los cambios del servidor en cuanto llegan.
flowchart LR APP[tu aplicacion] -->|consulta local instantanea| LDB[base de datos local] LDB <-->|replicacion bidireccional| ENG[motor de sincronizacion] ENG <-->|cambios en streaming| SDB[base de datos del servidor] style LDB fill:#a6e3a1,color:#11111b style ENG fill:#cba6f7,color:#11111b
La consecuencia para quien programa es enorme: desaparecen la capa de API, la gestión manual del caché, la invalidación, los estados de carga y de error de cada petición. Tu código se parece más al de una app de escritorio de los noventa que consulta su base de datos local, con la diferencia de que esa base de datos está mágicamente sincronizada con un servidor y con los demás usuarios. El motor absorbe toda la complejidad que en el nivel treinta y seis escribiste a mano: la cola durable de escrituras, el drenado al reconectar, los reintentos, la reconciliación de identificadores. Lo que era tu problema pasa a ser su problema.
Conviene distinguir dos direcciones que todo motor gestiona por separado. El camino de lectura —del servidor al cliente— replica hacia abajo los datos que a este usuario le tocan y los mantiene frescos ante cada cambio en el servidor. El camino de escritura —del cliente al servidor— toma las mutaciones locales, las encola y las sube, donde el servidor las valida y, si las acepta, se convierten en cambios que el camino de lectura propaga de vuelta a todas las réplicas. Algunos motores son fuertes sobre todo en el camino de lectura y dejan la escritura como una API tradicional; otros cierran el círculo entero. Saber cuál de los dos caminos te importa más es la primera pregunta al elegir.
Cuatro motores, cuatro apuestas
La categoría es joven y sus miembros divergen en qué optimizan y en qué base de datos del servidor asumen. Conviene conocer las cuatro apuestas dominantes.
Electric SQL
Centrado en Postgres. Sincroniza subconjuntos de tablas llamados shapes hacia el cliente mediante replicación lógica servida por HTTP. Te quedas en tu Postgres de siempre y Electric mantiene vivas las shapes que declaras.
Zero
De los creadores de Replicache. La consulta es la unidad de sincronización: escribes consultas en ZQL y Zero replica y mantiene vivos sus resultados en el cliente con mantenimiento incremental de vistas. Sincronizas lo que consultas.
Jazz
Sube el nivel: no es solo sync, es un framework local-first completo sobre CRDTs. Sus CoValues traen colaboración, permisos por grupos y cifrado de extremo a extremo integrados. Menos base de datos, más plataforma.
PowerSync
Empareja un SQLite en el cliente con Postgres, MongoDB o MySQL en el servidor. Unas reglas de sincronización deciden qué filas recibe cada usuario, y una cola de subida propaga las escrituras. Pragmático y multiplataforma.
Estas apuestas no son intercambiables. Electric SQL y PowerSync parten de que ya tienes una base de datos relacional y una fuente de verdad en el servidor, y se limitan a proyectar subconjuntos de ella al cliente; son la elección natural cuando modernizas un backend existente. Zero reimagina la relación entre consulta y datos, tratando cada consulta viva como una suscripción reactiva que el servidor alimenta, y brilla cuando quieres una experiencia reactiva de extremo a extremo sin pensar en caché. Jazz es el más ambicioso y el más opinado: al construirse sobre CRDTs y traer permisos y cifrado de fábrica, acerca la promesa completa del manifiesto local-first, a cambio de adoptar su modelo de datos en lugar del tuyo.
// Electric: una shape declara el subconjunto de una tabla que se sincroniza
const stream = new ShapeStream({
url: 'https://api.ejemplo/v1/shape',
params: { table: 'tareas', where: "proyecto_id = '42'" },
})
// el cliente lee de un store local que Electric mantiene al dia
// Zero: la consulta ES la unidad de sincronizacion; su resultado sigue vivo
const tareas = z.query.tarea
.where('proyectoId', '=', '42')
.where('completada', '=', false)
.orderBy('creada', 'desc')
// el resultado se refresca solo cuando el servidor cambia
El contraste con Jazz es instructivo, porque no proyecta filas de una base de datos ajena sino que los datos nacen ya como CRDTs con permisos incorporados. En lugar de una tabla en el servidor y una shape que la filtra, tienes un grafo de valores colaborativos cuya visibilidad la decide un grupo, y el sync y el cifrado son consecuencia del modelo, no una capa añadida.
// Jazz: los datos son CoValues, CRDTs con permisos por grupo integrados
const lista = ListaDeTareas.create({ titulo: 'Compra' }, { owner: grupo })
lista.push(Tarea.create({ texto: 'Pan', hecha: false }, { owner: grupo }))
// el grupo decide quien lee y escribe; el sync y el cifrado van de fabrica
Replicación parcial: el muro que todos rodean
Hay un motivo por el que los cuatro motores necesitan una shape, una consulta o una regla, y no basta con decir sincroniza todo: la base de datos del servidor no cabe en el cliente. Un Postgres de producción tiene gigabytes o terabytes; un teléfono no puede ni debe replicarlos. Por eso la replicación parcial es el problema central de la categoría, y la forma de declarar qué subconjunto viaja es la decisión de diseño que distingue a cada motor. Electric la llama shape —un filtro sobre una tabla—; Zero la deriva de tus consultas; PowerSync la codifica en reglas de sincronización que mapean usuarios a filas; Jazz la resuelve por el grafo de CoValues a los que un usuario tiene acceso. Distintos nombres para la misma pregunta: qué porción del universo de datos merece vivir en este dispositivo, para este usuario, ahora.
Esa pregunta arrastra dos más que ningún motor puede esquivar. La primera es la autorización: si el cliente recibe un subconjunto de la base de datos, el motor debe garantizar que ese subconjunto es exactamente el que el usuario tiene permiso de ver, porque una shape mal filtrada es una fuga de datos. La segunda es la reactividad del propio subconjunto: cuando cambian los criterios —el usuario abre otro proyecto, gana un permiso, filtra distinto— la porción replicada debe crecer o encogerse sola, sin descargar de nuevo lo que ya tenía. Resolver bien la replicación parcial, con su autorización y su reactividad, es lo que separa un motor de juguete de uno de producción, y es donde estas herramientas concentran su ingeniería más difícil.
Merece la pena ver la continuidad. La cola durable de mutaciones que escribiste a mano, el detector de reconexión, el backoff, el mapa de id provisional a id real: todo eso vive dentro de un motor de sincronización, ya escrito, probado y afinado por gente que solo trabaja en ese problema. Lo que cambia no es el concepto sino quién carga con la implementación. Por eso aquel ejercicio manual no fue tiempo perdido: es el modelo mental que te permite entender qué hace un motor por debajo y depurarlo cuando su abstracción se rompe.
Adoptar un motor de sincronización no hace desaparecer la complejidad del estado distribuido: la traslada. Dejas de escribir la cola de mutaciones, sí, pero heredas la carga de modelar bien tus shapes o reglas, de razonar sobre qué se replica a quién, de depurar por qué un cliente ve datos rancios y de entender la política de conflictos que el motor elige por ti —a menudo last-write-wins o autoridad del servidor, no siempre CRDTs—. La abstracción es potente, pero es porosa: cuando falla, necesitas entender la mecánica que estudiaste a mano para diagnosticarla. El motor te ahorra escribir el código difícil; no te exime de comprenderlo.
La forma madura de ver esta categoría es entender que no es una librería más de acceso a datos, sino un candidato a reemplazar una capa entera de la arquitectura que dábamos por eterna: la API. Durante veinte años la frontera entre cliente y servidor fue una interfaz de peticiones —REST, GraphQL, RPC— que traducía consultas en respuestas, y alrededor de ella construimos un ecosistema inmenso de caché, invalidación, serialización, estados de carga y reintentos, todo para paliar el hecho de que los datos vivían al otro lado de una red poco fiable. El motor de sincronización disuelve esa frontera al mover una réplica de los datos al lado del cliente y encargarse él de mantenerla coherente. Cuando lo consigue, la API deja de existir como concepto en tu código: no hay endpoints que diseñar ni respuestas que cachear, solo una consulta local y una promesa de que estará fresca. Fíjate en que esto cierra el arco del track entero. Empezaste distinguiendo estado de cliente y estado de servidor como dos mundos con herramientas distintas; el motor de sincronización los funde en uno, porque el estado del servidor se vuelve una consulta local más, indistinguible de la que antes llamabas estado de cliente. La distinción que organizó tu forma de pensar el estado durante años era, se descubre, un artefacto de la red, no una verdad sobre los datos. Y cuando la infraestructura borra un artefacto que creíamos fundamental, no está optimizando: está redibujando el mapa. La próxima década de aplicaciones se construirá, para una clase amplia de problemas, sobre este redibujo, y quien entienda que el motor de sincronización ocupa el lugar del backend tendrá el mapa antes de que se imprima.
- Toma la cola de mutaciones offline que escribiste en el nivel treinta y seis y enumera, punto por punto, qué piezas suyas reemplazaría un motor de sincronización.
- Levanta Electric SQL o PowerSync sobre un Postgres de juguete y define una shape o una regla que sincronice solo las filas de un usuario. Comprueba que el cliente no recibe las demás.
- Con la app corriendo, cambia una fila directamente en Postgres y observa cómo el cambio aparece solo en el cliente, sin recargar.
- Desconecta la red, escribe en el cliente y reconecta. Verifica que el motor drena tus escrituras y las reconcilia, tal como hacía tu cola manual.
- Investiga qué política de conflictos usa el motor que elegiste y contrasta si son CRDTs, last-write-wins o autoridad del servidor.
- Fuerza un fallo de autorización pidiendo datos fuera de tu shape y confirma que el motor no te los entrega. Explica por qué la replicación parcial es también una frontera de seguridad.