Saltar al contenido
Hugo Méndez
  • PWA
  • Offline
  • Arquitectura
  • React

Apps que funcionan sin internet: sincronización offline con una "outbox"

Cómo diseñar una app web o móvil que siga funcionando sin señal y sincronice después sin perder ni duplicar datos: réplica local, cola de operaciones, idempotencia y conflictos.

Hugo Méndez1 de septiembre de 20263 min de lectura

Técnicos en campo, almacenes con mala señal, entrenamientos en una cancha, repartidores en ruta: hay muchos lugares donde internet falla justo cuando el usuario está capturando algo importante. Si tu app depende de la conexión, esos datos se pierden.

La arquitectura que uso para resolverlo se llama local-first y gira alrededor de una idea sencilla: la app escribe primero en el dispositivo y sincroniza cuando puede.

Las piezas

  1. Réplica local (IndexedDB en web, SQLite en móvil): una copia de los datos que el usuario necesita.
  2. Outbox: una cola persistente con cada cambio pendiente de enviar.
  3. Push: enviar la cola al servidor en lotes.
  4. Pull: traer lo que cambió en el servidor desde la última vez.
  5. Resolución de conflictos: qué hacer cuando dos dispositivos cambiaron lo mismo.

Cada cambio es una operación con identidad

En la outbox no guardes “el estado del registro”, guarda operaciones:

type Operacion = {
  operationId: string;   // UUID generado en el dispositivo
  entityType: 'sesion' | 'medicion' | 'tarea';
  entityId: string;
  action: 'create' | 'update' | 'delete';
  payload: unknown;
  baseVersion: number;   // versión del registro que el usuario tenía
  clientTimestamp: string;
};

El operationId es la clave de todo: permite que el servidor aplique cada operación una sola vez, aunque llegue repetida porque se cortó la conexión justo después de enviarla.

El servidor: idempotente y honesto

Por cada operación del lote, el servidor responde uno de estos estados:

  • Aplicada: se guardó, aquí está la versión nueva. Si ya se había aplicado antes, regresa la misma respuesta guardada.
  • Rechazada: no es válida o el usuario no tiene permiso. No tiene caso reintentar.
  • Conflicto: la versión cambió en el servidor desde que el usuario la leyó.
  • Reintentar: falla temporal; el cliente vuelve a intentarlo después.

Un detalle importante: una operación rechazada no debe tumbar el resto del lote. Y las operaciones siguientes sobre el mismo registro deben esperar a que la anterior se resuelva, para respetar el orden.

Pull con cursor

Para traer cambios, el servidor mantiene un registro de cambios y le da al cliente un cursor opaco. El cliente aplica el lote en una transacción local y solo entonces avanza su cursor. Los borrados viajan como tombstones (marcas de “este registro ya no existe”) para que las réplicas también los eliminen.

Conflictos: decide por tipo de dato

No hay una regla universal:

  • La última escritura gana: bien para preferencias o datos de una sola persona.
  • Fusionar: si cada dispositivo cambió campos distintos.
  • Preguntar al usuario: cuando el dato importa y no hay forma segura de decidir. Mostrar “tienes un conflicto en esta sesión, ¿cuál versión conservamos?” es mejor que perder datos en silencio.

Detalles que hacen la diferencia

  • Una réplica por usuario: si alguien cierra sesión y entra otra persona en el mismo dispositivo, no deben mezclarse datos.
  • Estado visible: el usuario debe ver si algo está pendiente, fallido o en conflicto.
  • Sincroniza en los momentos correctos: al recuperar conexión, al volver la app a primer plano y periódicamente mientras está abierta.
  • Service Worker para que la app abra sin conexión.

No todas las apps necesitan esto, pero cuando el trabajo ocurre lejos del Wi-Fi, es lo que separa una app que la gente usa de una que abandona. Si tu operación tiene ese problema, platiquemos.

Volver al blog

Servicios relacionados

¿Tienes un proceso que ya no aguanta más Excel?

Cuéntame qué necesitas. En menos de 24 horas hábiles te respondo con una idea clara de cómo resolverlo y cuánto cuesta.