Volver al Blog
AppsDesarrolloSoftware

Sincronización en Segundo Plano y Arquitectura Offline-First en React Native con Expo SQLite y Supabase

Brayan Developer
4 min de lectura
Sincronización en Segundo Plano y Arquitectura Offline-First en React Native con Expo SQLite y Supabase
Domina la arquitectura offline-first en aplicaciones móviles con React Native, Expo SQLite y resolución de conflictos de sincronización bidireccional en Supabase.

En aplicaciones móviles destinadas a operaciones de campo, ventas en ruta, inspecciones técnicas o logística, la conectividad a Internet nunca está garantizada. Una aplicación que muestra spinners infinitos o bloquea la interfaz ante la falta de señal genera pérdidas operativas críticas. Diseñar con un enfoque Offline-First utilizando Expo SQLite como motor local y un motor de sincronización bidireccional hacia Supabase / PostgreSQL permite que la app funcione al 100% sin conexión y sincronice los cambios en segundo plano al recuperar red.

Portada

Principios de la Arquitectura Offline-First#

En una aplicación Offline-First, la interfaz de usuario nunca lee ni escribe directamente en la API remota. Todas las operaciones se realizan contra la base de datos local SQLite:

[UI React Native / Componentes]
               ▲
               │ (Lectura / Escritura Inmediata <5ms)
               ▼
     [Local DB: Expo SQLite]
        ├── Tablas de Entidades (Pedidos, Clientes, Items)
        └── Tabla: sync_queue (Colas de mutaciones pendientes)
               │
               ▼
 [Background Sync Service / NetInfo Listener]
               │ (Detecta conexión a Internet)
               ▼
     [Supabase Remote Database]
     - Validación de Timestamps
     - Resolución de Conflictos (Last-Write-Wins)

1. Esquema de Base de Datos Local con Cola de Sincronización#

En Expo SQLite (expo-sqlite/next), creamos una tabla sync_queue para almacenar cada inserción, actualización o eliminación realizada fuera de línea:

// src/database/schema.ts
import * as SQLite from 'expo-sqlite';

export async function initDatabase() {
  const db = await SQLite.openDatabaseAsync('app_offline.db');

  await db.execAsync(`
    PRAGMA journal_mode = WAL;

    CREATE TABLE IF NOT EXISTS orders (
      id TEXT PRIMARY KEY NOT NULL,
      client_name TEXT NOT NULL,
      total_amount REAL NOT NULL,
      status TEXT NOT NULL,
      created_at TEXT NOT NULL,
      updated_at TEXT NOT NULL,
      sync_status TEXT DEFAULT 'SYNCED' -- 'SYNCED', 'PENDING', 'CONFLICT'
    );

    CREATE TABLE IF NOT EXISTS sync_queue (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      table_name TEXT NOT NULL,
      operation TEXT NOT NULL, -- 'INSERT', 'UPDATE', 'DELETE'
      record_id TEXT NOT NULL,
      payload TEXT NOT NULL,
      created_at TEXT NOT NULL
    );
  `);

  return db;
}

2. Repositorio Local con Encolado Automático de Mutaciones#

Cuando el usuario crea una orden en el móvil:

// src/services/orderService.ts
import { SQLiteDatabase } from 'expo-sqlite';

export async function createLocalOrder(
  db: SQLiteDatabase,
  order: { id: string; clientName: string; totalAmount: number }
) {
  const now = new Date().toISOString();

  await db.withTransactionAsync(async () => {
    // 1. Insertar en tabla local para actualización visual inmediata
    await db.runAsync(
      `INSERT INTO orders (id, client_name, total_amount, status, created_at, updated_at, sync_status) 
       VALUES (?, ?, ?, 'PENDING', ?, ?, 'PENDING')`,
      [order.id, order.clientName, order.totalAmount, now, now]
    );

    // 2. Registrar en la cola de sincronización
    await db.runAsync(
      `INSERT INTO sync_queue (table_name, operation, record_id, payload, created_at) 
       VALUES ('orders', 'INSERT', ?, ?, ?)`,
      [order.id, JSON.stringify(order), now]
    );
  });
}

3. Motor de Sincronización en Segundo Plano con Expo Background Fetch#

Utilizamos expo-task-manager y expo-background-fetch junto con el listener de conectividad @react-native-community/netinfo:

// src/services/syncEngine.ts
import * as SQLite from 'expo-sqlite';
import NetInfo from '@react-native-community/netinfo';
import { supabase } from '../lib/supabase';

export async function processSyncQueue() {
  const state = await NetInfo.fetch();
  if (!state.isConnected) return;

  const db = await SQLite.openDatabaseAsync('app_offline.db');
  const pendingTasks = await db.getAllAsync<{
    id: number;
    table_name: string;
    operation: string;
    record_id: string;
    payload: string;
  }>('SELECT * FROM sync_queue ORDER BY id ASC LIMIT 50');

  if (pendingTasks.length === 0) return;

  for (const task of pendingTasks) {
    const data = JSON.parse(task.payload);

    try {
      if (task.operation === 'INSERT' || task.operation === 'UPDATE') {
        const { error } = await supabase
          .from(task.table_name)
          .upsert(data, { onConflict: 'id' });

        if (error) throw error;
      } else if (task.operation === 'DELETE') {
        const { error } = await supabase
          .from(task.table_name)
          .delete()
          .eq('id', task.record_id);

        if (error) throw error;
      }

      // Marcar como sincronizado y eliminar de la cola
      await db.runAsync('DELETE FROM sync_queue WHERE id = ?', [task.id]);
      await db.runAsync(
        `UPDATE ${task.table_name} SET sync_status = 'SYNCED' WHERE id = ?`,
        [task.record_id]
      );
    } catch (err) {
      console.error(`Error sincronizando tarea ${task.id}:`, err);
      // Detener para mantener el orden cronológico estricto
      break;
    }
  }
}

Estrategias de Resolución de Conflictos#

Cuando múltiples usuarios editan el mismo registro mientras están desconectados, se aplican las siguientes políticas:

  1. Last-Write-Wins (LWW) basado en Timestamp: El servidor acepta la versión con la fecha de modificación updated_at más reciente.
  2. Column-Level Merging: Se combinan los campos modificados individualmente si no interfieren entre sí.
  3. Escalado a Aprobación Manual: Si existen discrepancias de inventario crítico, el registro se marca en estado CONFLICT para revisión por un supervisor.

Conclusión#

Construir aplicaciones con arquitectura Offline-First en React Native y Expo elimina la dependencia de conexiones inestables, entregando una experiencia fluida, robusta y de nivel empresarial.

¿Necesitas desarrollar una app móvil nativa para operaciones de campo o logística con sincronización offline? Descubre nuestro trabajo en Desarrollo de Apps Móviles o solicita una cotización para tu proyecto.

Etiquetas
React NativeExpo SQLiteOffline-FirstSupabaseSincronización en Segundo PlanoMobile Apps
Compartir:XLinkedInWhatsApp

¿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.

Hablemos