Cuando empecé a diseñar Caladero, la app de pesca en la que trabajo, tuve que decidir pronto una cuestión que condiciona casi todo lo demás: ¿la app funciona solo si hay conexión a internet, o funciona siempre y la conexión es un extra? Esa pregunta es, en esencia, la diferencia entre una app tradicional "cloud-first" y una app "local-first". Elegí la segunda, y quiero explicar qué significa en la práctica, más allá de la definición de manual.
Qué es local-first en términos simples
Una aplicación local-first guarda los datos del usuario en el propio dispositivo como fuente de verdad principal, y trata la sincronización con un servidor remoto como una capa adicional, no como un requisito para que la app funcione. La diferencia con el enfoque tradicional no es solo técnica, es de prioridad: en una app cloud-first, si no hay conexión, la app se rompe o queda inutilizable; en una app local-first, sin conexión la app sigue funcionando con normalidad, y la sincronización ocurre cuando la conexión vuelve a estar disponible.
No es lo mismo que "modo offline" entendido como una funcionalidad añadida a posteriori. Muchas apps ofrecen una versión reducida y limitada cuando no hay red. Local-first invierte el orden: el diseño parte de que no hay red, y la conectividad es la excepción que se gestiona, no la base sobre la que se construye todo.
Por qué elegí este enfoque para una app de pesca
La razón fue puramente práctica, no ideológica. Quien pesca suele estar en zonas con cobertura móvil deficiente o inexistente: orillas de embalses, zonas de costa alejadas, ríos de montaña. Si Caladero necesitara conexión constante para guardar un punto de pesca o consultar los puntos ya guardados, la app sería inútil precisamente en el contexto donde se va a usar más. Por eso la decisión de que los puntos guardados, las notas y el propio mapa base descargado se almacenen localmente en el dispositivo fue de las primeras que tomé en el proyecto, antes incluso de diseñar la interfaz.
Cómo se ve esto en la práctica
En términos de arquitectura, esto significa que la base de datos local (en mi caso, una base de datos Room sobre SQLite) es la fuente de verdad para la interfaz. La pantalla de puntos guardados lee siempre de la base de datos local, nunca directamente de una llamada de red. El flujo aproximado es:
- El usuario guarda un punto nuevo. Se escribe inmediatamente en la base de datos local.
- La interfaz se actualiza al instante, porque lee de esa misma base de datos local, sin esperar ninguna respuesta de red.
- En segundo plano, si hay conexión disponible, un proceso de sincronización envía ese punto nuevo al servidor y marca el registro local como sincronizado.
- Si no hay conexión, el punto queda marcado como "pendiente de sincronizar" y el propio dispositivo reintenta más adelante, sin que el usuario tenga que hacer nada ni note ninguna diferencia en el uso normal de la app.
Este patrón, guardar local primero y sincronizar después, es la esencia técnica del enfoque, y es bastante distinto de la lógica típica de "llamar a la API, esperar respuesta, y solo entonces actualizar la pantalla", que es como se construyen muchas apps por defecto.
Lo que gana el usuario
La ventaja más directa es velocidad percibida: guardar un punto en el mapa es instantáneo, no hay ninguna espera de red de por medio, porque la escritura local es prácticamente inmediata. La segunda ventaja es fiabilidad en condiciones reales de uso: la app sigue funcionando igual de bien con cobertura completa que sin cobertura alguna, algo que para un caso de uso como la pesca no es un extra, es un requisito básico.
Lo que cuesta a cambio: la complejidad de sincronizar
Aquí es donde local-first deja de sonar tan sencillo. El problema difícil no es guardar datos localmente, eso es relativamente trivial con las herramientas actuales. El problema difícil es la sincronización cuando hay conflictos: ¿qué pasa si el mismo punto se edita desde dos dispositivos distintos mientras ambos estaban sin conexión, y luego los dos recuperan la conexión casi a la vez?
No existe una respuesta universal correcta a esa pregunta; depende del tipo de dato. Para algo como notas de texto sobre un punto, una estrategia razonable es "gana la última edición por marca de tiempo", aceptando que en el caso raro de edición simultánea se pierda la versión más antigua. Para otros tipos de datos, esa pérdida silenciosa no sería aceptable y haría falta fusionar cambios o pedir al usuario que resuelva el conflicto manualmente. En Caladero, dado que cada usuario gestiona principalmente sus propios puntos privados desde un único dispositivo habitual, este escenario de conflicto es poco frecuente, pero aun así hay que diseñarlo desde el principio, no improvisarlo cuando ya aparece en producción.
Otro coste real es el de las migraciones de esquema local. Cuando cambias la estructura de la base de datos local en una nueva versión de la app, tienes que asegurarte de que los usuarios con datos guardados en dispositivos que llevan tiempo sin actualizar puedan migrar sin perder información, algo que en un backend centralizado se gestiona de forma mucho más controlada porque solo hay una copia de los datos, no una copia por cada dispositivo de cada usuario.
Errores que cometí al principio
Al principio subestimé cuánto código adicional hace falta solo para gestionar el estado de sincronización de cada registro (pendiente, sincronizado, en conflicto, con error). Es tentador pensar que basta con un booleano sincronizado: true/false, pero en la práctica hacen falta más estados intermedios para poder mostrar al usuario, de forma honesta, qué está pasando con sus datos: por ejemplo, distinguir entre "esperando conexión" y "hubo un error en el último intento de sincronizar", porque son situaciones que requieren mensajes y acciones distintas.
También aprendí que local-first no significa "sin backend". Sigue haciendo falta un servidor si se quiere sincronizar entre dispositivos o hacer copia de seguridad remota; lo que cambia es que ese servidor deja de ser un requisito para el funcionamiento básico de la app y pasa a ser un servicio adicional que mejora la experiencia cuando está disponible.
Conclusión
Local-first no es una moda técnica, es una decisión de arquitectura que se justifica cuando el contexto real de uso de la app incluye conexión intermitente o inexistente, como es el caso de la pesca. La ventaja para el usuario es clara: la app funciona siempre, no solo cuando hay cobertura. El coste está en la complejidad de sincronización, que hay que diseñar con cuidado desde el principio en vez de tratarla como un detalle que se resuelve más adelante. En mi experiencia con Caladero, ese coste inicial ha merecido la pena porque resuelve exactamente el problema que la app está pensada para resolver.
