Cambiar idioma:

Cómo construí una app que se sincroniza con Supabase sin sobreescribir datos - 02/03/2026

Una de las partes más desafiantes cuando construyes una app colaborativa es lograr que los datos se mantengan sincronizados sin que una actualización sobreescriba los cambios de otro usuario. En este post explico cómo resolví ese problema usando Supabase.

App en producción: capturioo.com

Una de las partes más desafiantes cuando construyes una app colaborativa o en tiempo real es lograr que los datos se mantengan sincronizados entre múltiples usuarios o sesiones, sin que una actualización de un cliente sobreescriba los cambios de otro. En este post, explico cómo resolví ese problema usando Supabase como backend.


El problema: sincronización vs. sobreescritura

En una app tradicional con fetch o axios, el flujo típico es:

  1. El cliente obtiene los datos del servidor.
  2. El usuario modifica algo.
  3. El cliente envía el estado completo de vuelta al servidor.

El problema es que en el paso 3, si otro usuario modificó el mismo recurso mientras tanto, esos cambios se pierden. Esto se llama sobreescritura ciega (blind overwrite).


La solución: actualizaciones granulares + tiempo real

En vez de enviar el objeto completo, la clave está en enviar solo el campo que cambió. A esto lo llamo patch semántico.

Ejemplo:

// ❌ Forma incorrecta — sobreescribe todo
await supabase
  .from('items')
  .update({ ...fullObject })
  .eq('id', item.id);

// ✅ Forma correcta — solo actualiza el campo modificado
await supabase
  .from('items')
  .update({ title: newTitle })
  .eq('id', item.id);

Supabase Realtime: escuchar cambios sin polling

Con supabase.channel() me suscribo a los cambios de la tabla en tiempo real. Cuando otro usuario modifica un registro, mi cliente recibe el evento y solo actualiza ese campo en el estado local, sin reemplazar todo el objeto:

supabase
  .channel('items-channel')
  .on(
    'postgres_changes',
    { event: 'UPDATE', schema: 'public', table: 'items' },
    (payload) => {
      // Solo fusiono el campo que cambió, no reemplazo el objeto completo
      setState((prev) =>
        prev.map((item) =>
          item.id === payload.new.id
            ? { ...item, ...payload.new }
            : item
        )
      );
    }
  )
  .subscribe();

Row Level Security (RLS): proteger sin bloquear

Supabase usa PostgreSQL con RLS. Configuré las políticas para que cada usuario solo pueda modificar sus propios registros, pero pueda leer los de otros en contextos colaborativos. Esto garantiza que aunque la sincronización sea abierta, los datos privados estén protegidos.


Lecciones aprendidas


Puedes ver esta arquitectura en funcionamiento en capturioo.com.