Arquitectura Offline-First en Apps Móviles con React Native, Expo y SQLite

En sectores como logística, inspecciones de campo, minería, ventas en ruta o delivery, la conectividad a internet móvil suele ser intermitente o inexistente. Diseñar bajo el paradigma Offline-First garantiza que tu aplicación móvil nunca se congele y continúe funcionando con total fluidez en cualquier circunstancia.

¿Qué significa realmente construir una App Offline-First?#
El enfoque tradicional de desarrollo móvil (Online-First) asume que la red siempre está disponible. Si el usuario pierde la señal 4G/5G, la app muestra una pantalla de error o spinners infinitos, bloqueando la captura de datos esenciales.
Por el contrario, una arquitectura Offline-First invierte la jerarquía de persistencia:
- La base de datos local es la única fuente de verdad inmediata de la interfaz (UI): Toda lectura o escritura se realiza instantáneamente sobre un motor local embebido (SQLite con
expo-sqlite, WatermelonDB o PowerSync). - La sincronización con el servidor backend ocurre en segundo plano: Un motor de sincronización (Sync Engine) detecta la conectividad y transmite los cambios pendientes en lotes estructurados.
- Resolución de conflictos determinista: Si dos usuarios modifican el mismo registro mientras uno de ellos estaba offline, se aplican estrategias como Last-Write-Wins (LWW) o control de versiones por marcas de tiempo (CRDTs).
Estructura de la Base de Datos Local con expo-sqlite#
Con el nuevo API síncrono y promesas de expo-sqlite, gestionar bases de datos relacionales en dispositivos iOS y Android es extremadamente rápido y seguro:
// src/lib/db.ts
import * as SQLite from "expo-sqlite";
export const db = SQLite.openDatabaseSync("app_offline.db");
export function initDatabase() {
// Tabla de órdenes de trabajo con estado de sincronización
db.execSync(`
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS work_orders (
id TEXT PRIMARY KEY NOT NULL,
client_name TEXT NOT NULL,
status TEXT NOT NULL,
notes TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
sync_status TEXT DEFAULT 'pending' -- 'pending' | 'synced' | 'conflict'
);
CREATE TABLE IF NOT EXISTS sync_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_name TEXT NOT NULL,
record_id TEXT NOT NULL,
action TEXT NOT NULL, -- 'INSERT' | 'UPDATE' | 'DELETE'
payload TEXT NOT NULL,
timestamp INTEGER NOT NULL
);
`);
}
El Patrón de Cola de Sincronización (Sync Queue)#
Cuando el usuario crea o edita un registro en modo offline, ejecutamos una transacción atómica que actualiza la tabla local y encola la mutación:
// src/services/workOrders.ts
import { db } from "@/lib/db";
import * as Crypto from "expo-crypto";
export function createWorkOrder(clientName: string, notes: string) {
const id = Crypto.randomUUID();
const now = Date.now();
const payload = { id, clientName, status: "in_progress", notes, created_at: now, updated_at: now };
// Transacción segura: Guardar localmente y agregar a la cola
db.withTransactionSync(() => {
db.runSync(
`INSERT INTO work_orders (id, client_name, status, notes, created_at, updated_at, sync_status)
VALUES (?, ?, ?, ?, ?, ?, 'pending')`,
[id, clientName, "in_progress", notes, now, now]
);
db.runSync(
`INSERT INTO sync_queue (table_name, record_id, action, payload, timestamp)
VALUES ('work_orders', ?, 'INSERT', ?, ?)`,
[id, JSON.stringify(payload), now]
);
});
return id;
}
Motor de Sincronización en Segundo Plano con @react-native-community/netinfo#
El servicio de sincronización escucha cambios en la conectividad del dispositivo y procesa la cola:
// src/services/syncEngine.ts
import NetInfo from "@react-native-community/netinfo";
import { db } from "@/lib/db";
let isSyncing = false;
export function startSyncListener() {
NetInfo.addEventListener((state) => {
if (state.isConnected && state.isInternetReachable) {
processSyncQueue();
}
});
}
export async function processSyncQueue() {
if (isSyncing) return;
isSyncing = true;
try {
const queueItems: any[] = db.getAllSync(`SELECT * FROM sync_queue ORDER BY timestamp ASC`);
for (const item of queueItems) {
const payload = JSON.parse(item.payload);
const response = await fetch("https://api.tudominio.com/api/sync", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: item.action,
table: item.table_name,
data: payload,
}),
});
if (response.ok) {
// Marcar como sincronizado y eliminar de la cola
db.withTransactionSync(() => {
db.runSync(`DELETE FROM sync_queue WHERE id = ?`, [item.id]);
db.runSync(`UPDATE work_orders SET sync_status = 'synced' WHERE id = ?`, [item.record_id]);
});
}
}
} catch (error) {
console.error("Error durante sincronización:", error);
} finally {
isSyncing = false;
}
}
Beneficios para Operaciones Empresariales#
- Cero Latencia en la Interfaz: Las operaciones de lectura y guardado tardan menos de 5 milisegundos porque ocurren de forma local.
- Ahorro de Batería y Datos Móviles: La sincronización por lotes comprime y envía datos únicamente cuando es necesario.
- Continuidad Operativa Garantizada: Los trabajadores en terreno pueden registrar inventarios, firmas digitales y reportes en zonas sin cobertura sin temor a perder información.
¿Planeas Desarrollar una Aplicación Móvil Empresarial?#
Diseñar una aplicación con soporte offline robusto requiere experiencia en bases de datos embebidas, manejo de estado reactivo y protocolos de sincronización resilientes.
Si quieres crear una app móvil nativa para iOS y Android con React Native y Expo, contáctanos o conoce más sobre nuestros servicios de desarrollo móvil.
¿Te gustaría profundizar en estos temas?
Aprende sobre desarrollo de software, apps a medida, automatizaciones con N8N, Next.js y Cloud con casos reales.


