---
term: 'Sincronización de Datos Offline-First'
seoTitle: 'Sincronización Offline-First: Store Local, Conflictos y Replay'
headline: '¿Qué es la sincronización de datos offline-first?'
slug: sincronizacion-offline-first
category: api-realtime
shortDefinition: 'La sincronización offline-first es una arquitectura donde la app lee y escribe primero en un store local y luego reconcilia con el servidor al volver online.'
relatedTerms:
  - real-time-live-queries
  - websockets-real-time-sync
  - push-notifications-apns-fcm
  - cross-platform-development
contrastsWith:
  - real-time-live-queries
faq:
  - question: '¿Qué es una app offline-first?'
    answer: 'Una app cuyas pantallas leen y escriben en una base de datos en el propio dispositivo, con la red degradada a un detalle de sincronización. Cada toque funciona en modo avión porque nada en el camino de la interacción espera a un servidor; una capa de sync en segundo plano reconcilia el store local con el backend cada vez que la conectividad lo permite. Contrasta con las apps online-first, que muestran spinners hasta que el servidor responde.'
  - question: '¿Cómo funciona la sincronización de datos offline?'
    answer: 'Con dos loops. Salida: las escrituras locales caen en el store del dispositivo y en una cola de outbox durable; cuando la red vuelve, la cola se reproduce contra el servidor en orden. Entrada: el cliente descarga los cambios desde su último sync — por timestamp, número de secuencia o change feed — y los aplica localmente. La resolución de conflictos vive donde los dos loops se encuentran, decidiendo qué pasa cuando ambos lados editaron el mismo registro.'
  - question: '¿Qué es last-write-wins y cuándo es seguro?'
    answer: 'La política de conflictos más simple: comparar timestamps o versiones y quedarse con la escritura más nueva, descartando la otra en silencio. Es segura cuando las ediciones son de registro completo y de bajo riesgo — un toggle de configuración, una flag de estado — o cuando lo normal es un solo escritor por registro. Es peligrosa en documentos compartidos y contadores, donde "descartar la otra edición" significa perder el trabajo de alguien.'
  - question: '¿Cómo resuelven conflictos los CRDTs?'
    answer: 'Haciendo que el conflicto sea irrepresentable: un conflict-free replicated data type restringe cada campo a operaciones que se fusionan de forma determinista sin importar el orden de llegada — contadores que solo crecen, conjuntos de add/remove, registros de último escritor. Dos réplicas que intercambian estado siempre convergen sin coordinación. El costo es disciplina de modelado: tus datos deben expresarse en vocabulario CRDT, y las bibliotecas CRDT completas son maquinaria pesada para la app móvil típica de formularios y listas.'
  - question: '¿Qué pasa con las escrituras hechas offline?'
    answer: 'Se encolan. Un outbox durable registra cada operación pendiente, y el store local refleja la escritura de inmediato para que la interfaz sea fiel a la intención del usuario. Al reconectar, la cola se reproduce en orden; las fallas necesitan política — reintentar errores transitorios, y devolver a la interfaz los rechazos de permisos y validación en lugar de reintentar para siempre. Las operaciones idempotentes hacen que el replay sea seguro cuando se pierde un acknowledgment.'
  - question: '¿Offline-first es lo mismo que caché?'
    answer: 'No — la diferencia es la dirección. Una caché acelera lecturas: el servidor sigue siendo la fuente de la verdad y las entradas viejas simplemente se vuelven a descargar. Offline-first además acepta escrituras localmente, y eso es lo que crea divergencia entre dispositivo y servidor y obliga a la maquinaria difícil — colas de outbox, replay, resolución de conflictos. La caché de lectura es una optimización de rendimiento; offline-first es un compromiso de arquitectura de datos.'
  - question: '¿Cuándo no deberías construir offline-first?'
    answer: 'Cuando la corrección depende de un único estado autoritativo: pagos, reservas, descuentos de inventario, cualquier cosa donde dos dispositivos actuando sobre datos viejos crean gastos dobles en el mundo real. También cuando los datos se calculan en el servidor y son mayormente de lectura — un feed de noticias degrada bien con una caché simple. La maquinaria de sync es un impuesto permanente; págalo donde las condiciones de campo o las expectativas del usuario lo exijan.'
  - question: '¿Cómo fijan datos localmente los SDKs móviles?'
    answer: 'Fijar (pin) marca objetos para almacenamiento durable en el dispositivo: los registros fijados sobreviven reinicios, se pueden consultar offline con la misma interfaz de consulta que la del servidor y siguen ligados a sus identidades en el servidor, de modo que los syncs posteriores los actualizan en su lugar. Combinado con escrituras encoladas — la semántica de save-eventually — el pinning entrega una capa offline funcional sin armar a mano un esquema de base de datos en el dispositivo.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'Conflict-free replicated data type (Wikipedia)'
    url: 'https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type'
  - name: 'Eventual consistency (Wikipedia)'
    url: 'https://en.wikipedia.org/wiki/Eventual_consistency'
  - name: 'Version Vector — Patterns of Distributed Systems (martinfowler.com)'
    url: 'https://martinfowler.com/articles/patterns-of-distributed-systems/version-vector.html'
  - name: 'Local Datastore — Parse iOS SDK guide'
    url: 'https://docs.parseplatform.org/ios/guide/#local-datastore'
cta:
  title: 'Publica apps que funcionan en modo avión'
  text: 'Los SDKs móviles de Back4app incluyen un Local Datastore listo para usar — fija (pin) consultas para lecturas offline, encola escrituras con save-eventually y deja que el backend gestionado sea la fuente de la verdad contra la que tus dispositivos reconcilian.'
  linkText: 'Empieza gratis'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-09-04'
translationKey: offline-first-data-sync
---

**La sincronización offline-first es una arquitectura donde la app lee y escribe primero en un store local y luego reconcilia con el servidor al volver online.** La red deja de ser una dependencia y se convierte en un proceso de segundo plano — cada pantalla se renderiza desde el dispositivo, cada toque se confirma en el dispositivo, y una capa de sync ajusta cuentas con el backend cuando la conectividad lo permite. Lo difícil nunca fue guardar datos localmente; es lo que pasa cuando dos copias de la verdad no están de acuerdo.

## Puntos clave

| Pregunta | Respuesta |
| --- | --- |
| El movimiento central | Las pantallas hablan con un store local en el dispositivo; la red sincroniza en segundo plano |
| Las dos posturas | Store local como *buffer* (el servidor es dueño de la verdad) o como *réplica* (la verdad se negocia) |
| Escrituras offline | Cola de outbox durable → replay en orden al reconectar |
| Conflictos | Last-write-wins · merge por campo · CRDTs — elige por campo, no por app |
| El costo honesto | La maquinaria de sync es complejidad permanente — gástala donde el offline importa |

## Fijar, leer, encolar: el loop offline en código

La versión del patrón en los SDKs móviles — fija lo que las pantallas necesitan, encola lo que los usuarios cambian:

**JavaScript:**

```javascript
// JavaScript — Back4app JS SDK with the Local Datastore enabled
Parse.enableLocalDatastore();

// Read: serve the screen from the local store — instant, works offline
const query = new Parse.Query('Task');
query.fromLocalDatastore();
render(await query.find());

// Write: durable locally now, queued for the server automatically
const task = new Parse.Object('Task');
task.set('title', 'Inspect site 14');
task.set('done', false);
await task.pin();          // survives restarts, feeds local reads
task.saveEventually();     // replays when the network returns
```

**Flutter:**

```dart
// Flutter / Dart — Back4app Flutter SDK with local storage enabled
// Parse().initialize(..., coreStore: await CoreStoreSembastImp.getInstance())

// Read: serve the screen from pinned data — instant, works offline
final pinned = await ParseObject('Task').fromPin('openTasks');
render(pinned);

// Write: durable locally now, synced when the network allows
final task = ParseObject('Task')
  ..set('title', 'Inspect site 14')
  ..set('done', false);
await task.pin();                 // survives restarts, feeds local reads

final response = await task.save();
if (!response.success) retryLater(task);   // replay from the outbox
```

**Swift:**

```swift
// iOS / Swift — Back4app iOS SDK with the Local Datastore enabled
// (in the app delegate) Parse.enableLocalDatastore()

// Read: serve the screen from the local store — instant, works offline
let query = PFQuery(className: "Task")
query.fromLocalDatastore()
query.findObjectsInBackground { tasks, _ in
  render(tasks)
}

// Write: durable locally now, queued for the server automatically
let task = PFObject(className: "Task")
task["title"] = "Inspect site 14"
task["done"] = false
task.pinInBackground()     // survives restarts, feeds local reads
task.saveEventually()      // replays when the network returns
```

**Kotlin:**

```kotlin
// Android / Kotlin — Back4app Android SDK with the Local Datastore enabled
// (before Parse.initialize) Parse.enableLocalDatastore(context)

// Read: serve the screen from the local store — instant, works offline
val query = ParseQuery.getQuery<ParseObject>("Task")
query.fromLocalDatastore()
query.findInBackground { tasks, _ ->
    render(tasks)
}

// Write: durable locally now, queued for the server automatically
val task = ParseObject("Task")
task.put("title", "Inspect site 14")
task.put("done", false)
task.pinInBackground()     // survives restarts, feeds local reads
task.saveEventually()      // replays when the network returns
```

Dos llamadas cargan con toda la arquitectura. *Fijar (pin)* hace que los objetos sean durables y consultables en el dispositivo — la misma interfaz de consulta que la del servidor, apuntada al almacenamiento local ([Local Datastore](https://docs.parseplatform.org/ios/guide/#local-datastore) en los SDKs de arriba). *Save-eventually* acepta la escritura ahora y se hace cargo de su entrega después. Todo el resto de este artículo es lo que ocurre entre esas dos llamadas.

## ¿Buffer o fuente de la verdad? La decisión detrás de la decisión

Todo diseño offline-first elige en silencio una de dos posturas para el store local, y la mayor parte del dolor de sync viene de no elegirla deliberadamente.

**Store local como buffer.** El servidor sigue siendo autoritativo; el dispositivo guarda una copia de trabajo más un outbox de intención. Los conflictos se resuelven a favor del servidor o por una política simple, y un dispositivo siempre puede repararse con un nuevo pull. Es el default correcto para apps de negocio — checklists de CRM, inspecciones en campo, captura de pedidos — porque el razonamiento se mantiene simple: la verdad vive en un solo lugar, los dispositivos solo van rezagados.

**Store local como réplica.** La verdad se *negocia* entre pares y el servidor es una réplica más — la postura de los editores colaborativos y las apps de notas que prometen semántica de "todo se fusiona". Esto compra resiliencia y confianza del usuario al precio de maquinaria real de sistemas distribuidos: version vectors para detectar concurrencia, funciones de merge por tipo de dato y [consistencia eventual](https://en.wikipedia.org/wiki/Eventual_consistency) como la garantía más fuerte que puedes prometer con honestidad.

La postura se elige por *conjunto de datos*, no por app: la misma app de servicio en campo puede tratar las órdenes de trabajo del propio técnico como réplica (deben ser editables todo el día bajo tierra) y el catálogo de repuestos como un buffer read-through.

## El loop de sync, de punta a punta

```mermaid
flowchart LR
  accTitle: Loop de sincronización offline-first
  accDescr: La app lee y escribe en un store local. Las escrituras también entran en una cola de outbox durable. Cuando la conectividad vuelve, la cola reproduce las operaciones ordenadas hacia el servidor, el servidor aplica resolución de conflictos contra las ediciones concurrentes, y el cliente descarga al store local los cambios ocurridos desde su último sync.
  UI["Pantallas de la app"] -->|"leer + escribir"| LS[("Store local")]
  UI -->|"cada escritura"| OB["Cola de outbox<br/>(durable, ordenada)"]
  OB -->|"replay al reconectar"| SV["Servidor<br/>(resolución de conflictos)"]
  SV -->|"cambios desde el último sync"| LS
  SV --- DB[("Base de datos del backend")]
```

El loop tiene una mitad de salida y una de entrada, y se encuentran en la resolución de conflictos.

**Salida: escrituras encoladas y replay.** Las escrituras offline se confirman localmente y se agregan a un outbox durable — la interfaz refleja la intención de inmediato, etiquetada honestamente como "pendiente" donde importa. Al reconectar, la cola se reproduce en orden. La ingeniería está en los modos de falla: los errores transitorios se reintentan con backoff; los rechazos de permisos y validación deben *aparecer ante el usuario*, no reintentarse eternamente; y las operaciones deberían ser idempotentes, porque un acknowledgment perdido significa que la misma operación puede llegar dos veces. Reproducir "marcar estado como hecho" dos veces es inofensivo; reproducir "incrementar stock en 3" dos veces es un bug.

**Entrada: pull de deltas.** Volver a descargar todo al reconectar desperdicia ancho de banda y batería; el sync de producción descarga *cambios desde un checkpoint* — una marca de agua de updated-at en los diseños simples, un número de secuencia del servidor o un change feed en los más robustos. Los mismos eventos que alimentan las [live queries](/glossary/es/live-queries-tiempo-real/) cuando la app está online sirven también como stream de deltas de entrada, y por eso el sync offline y el sync en tiempo real son un continuo, no dos features.

## Last-write-wins vs. merge vs. CRDTs

| Estrategia | Cómo resuelve | ¿Pierde datos? | Costo | Adecuada para |
| --- | --- | --- | --- | --- |
| Last-write-wins (la última escritura gana) | El timestamp/versión más nuevo se queda con el registro | **Sí — en silencio** | Trivial | Registros de un solo escritor, toggles, estados |
| Merge por campo | Las ediciones en campos distintos sobreviven; las colisiones en el mismo campo caen en la política | Solo en colisiones del mismo campo | Moderado | Formularios y registros editados por pocas personas |
| Resolución manual | Se guardan ambas versiones; un humano elige | No | Interfaz + flujo de trabajo | Registros de alto riesgo, consolas de sync |
| CRDT / CRDT-lite | Tipos de dato cuyas operaciones se fusionan de forma determinista | No | Disciplina de modelado, peso de la biblioteca | Contadores, conjuntos, estructuras colaborativas |

Dos notas honestas sobre esta tabla. Primera: los timestamps son más frágiles de lo que parecen — los relojes de los dispositivos se desvían, así que un last-write-wins serio usa la hora de recepción del servidor o versiones lógicas ([version vectors](https://martinfowler.com/articles/patterns-of-distributed-systems/version-vector.html) cuando las ediciones concurrentes deben *detectarse* y no adivinarse). Segunda: el punto dulce práctico para apps móviles es el **CRDT-lite**: las bibliotecas [CRDT](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) completas son maquinaria pesada, pero robarles la idea por campo es barato — modela una cantidad como *operaciones de incremento* en lugar de un total almacenado, las etiquetas como *conjuntos* de add/remove en lugar de un array, y esos campos simplemente dejan de entrar en conflicto. La estrategia se elige por campo; las apps que adoptan una sola política global casi siempre eligieron mal para algún campo.

## Casos de uso comunes

- **Operaciones en campo** — inspecciones, entregas, servicios públicos: el lugar de trabajo es exactamente donde la cobertura muere, así que la captura debe ser local y el sync, oportunista.
- **Notas y productividad personal** — un usuario, varios dispositivos: los conflictos son raros y casi siempre autoinfligidos, lo que vuelve manejables las políticas de merge.
- **Punto de venta y apps de kiosco** — la fila debe seguir avanzando durante el reinicio del router; las transacciones se reproducen cuando el enlace vuelve.
- **Apps de contexto de viaje** — itinerarios, mapas y boletos consumidos justo donde la conectividad es peor: aviones, trenes, roaming.
- **Productos para mercados emergentes y bajo ancho de banda** — las conexiones intermitentes y tarifadas hacen del delta-sync en segundo plano el único diseño respetuoso, sea cual sea la [mezcla de plataformas](/glossary/es/desarrollo-multiplataforma/).

## ¿Deberías construir offline-first? Matriz de decisión

| Construye offline-first cuando… | Quédate online-first cuando… |
| --- | --- |
| Los usuarios trabajan donde la cobertura falla — campo, tránsito, sótanos | Los usuarios están prácticamente siempre conectados (web de escritorio, herramientas de oficina) |
| Un toque bloqueado cuesta dinero o confianza (punto de venta, captura en campo) | La corrección exige un estado autoritativo *ahora* — pagos, reservas, inventario |
| Los datos son registros del propio usuario con pocos escritores concurrentes | Los datos se calculan en el servidor o son compartidos y calientes (feeds, dashboards) — cachea lecturas y mantén las escrituras online |
| Las escrituras toleran una reconciliación posterior | Dos dispositivos actuando sobre una verdad vieja son un riesgo del mundo real |
| Puedes financiar el mantenimiento permanente de la capa de sync | Un estado de carga es honestamente suficiente |

El camino intermedio es legítimo y común: cachea lecturas para pantallas instantáneas, permite escrituras offline solo en los pocos conjuntos de datos que de verdad las necesitan, y mantén el resto online-first detrás de estados de conectividad honestos.

## Limitaciones y trade-offs

- **La resolución de conflictos es una decisión de producto disfrazada de ingeniería.** "¿Qué edición sobrevive?" no tiene una respuesta técnicamente correcta — alguien debe ser dueño de la política por campo, y "quedarse en silencio con la más nueva" es una elección con víctima.
- **El outbox es una cola de pasivos.** Las escrituras pendientes de un dispositivo que reaparece después de dos semanas pueden reproducirse contra un mundo que cambió; las políticas de expiración y la validación en el replay son obligatorias, no paranoia.
- **Los bugs de sync son la peor clase de bug que puedes comprar.** Solo se reproducen bajo interleavings específicos de ediciones y conectividad — invierte en pruebas de replay determinista y en una superficie visible de estado de sync, o depura por anécdota para siempre.
- **La verdad local envejece.** Las pantallas deben ser honestas sobre la obsolescencia donde importa (precios, disponibilidad) — "actualizado hace 2 horas" es una feature, no una disculpa.
- **El almacenamiento y la identidad agregan fricción.** Los stores del dispositivo necesitan migraciones como cualquier base de datos, y los registros creados offline necesitan identidades generadas en el cliente que sobrevivan el viaje al servidor sin duplicarse.

## Sincronización offline-first en Back4app

Back4app es una plataforma open-source de Backend as a Service (BaaS) que combina base de datos gestionada, APIs REST y GraphQL generadas automáticamente, autenticación, almacenamiento de archivos y funciones serverless con Cloud Code. Sus SDKs móviles traen la postura de buffer lista: el Local Datastore fija los resultados de cualquier consulta para lecturas offline con la misma API de consulta, `saveEventually` le da a cada escritura un outbox durable con replay ordenado, y los triggers `beforeSave` de Cloud Code son el hogar natural de la política de conflictos del lado del servidor — chequeos de versión, reglas de merge, validación en el replay — aplicada en cada camino de escritura. Súmale el stream de [Live Query](/glossary/es/live-queries-tiempo-real/) para los deltas de entrada mientras la app está online, y el loop de sync del diagrama de arriba se vuelve configuración más política, no un subsistema que construyes.
