Cambiar idioma:

Cómo construí una app en tiempo real con Supabase - 07/03/2026

Construir una app en tiempo real solía requerir WebSockets personalizados y servidores dedicados. Con Supabase Realtime, todo eso desaparece. Esto es lo que aprendí construyendo capturioo.com.

App en producción: capturioo.com

Construir una app en tiempo real solía requerir WebSockets personalizados, servidores dedicados y una cantidad considerable de infraestructura. Con Supabase Realtime, todo eso desaparece y puedes tener datos sincronizados entre clientes en cuestión de minutos. Esto es lo que aprendí construyendo capturioo.com.

¿Qué significa “tiempo real” en una app?

Una app en tiempo real es aquella donde los cambios en los datos se propagan instantáneamente a todos los clientes conectados sin que el usuario tenga que refrescar la página. Los casos de uso son amplios:

El desafío técnico está en mantener esa sincronización de forma eficiente, segura y sin inconsistencias.

La arquitectura base: PostgreSQL + Realtime

Supabase expone una capa de tiempo real sobre PostgreSQL usando Logical Replication. Esto significa que cuando un registro cambia en la base de datos, Supabase transmite ese evento a todos los clientes suscritos a ese canal, ya sea a través de INSERT, UPDATE o DELETE.

No necesitas ningún servidor adicional. El canal de tiempo real es gestionado por Supabase.

Suscribirme a cambios: el patrón central

El patrón que usé en capturioo.com para escuchar cambios en tiempo real:

import { useEffect, useState } from 'react';
import { supabase } from '@/lib/supabase';

function useLiveCaptures() {
  const [captures, setCaptures] = useState<Capture[]>([]);

  useEffect(() => {
    // Carga inicial
    supabase
      .from('captures')
      .select('*')
      .order('created_at', { ascending: false })
      .then(({ data }) => {
        if (data) setCaptures(data);
      });

    // Suscripción en tiempo real
    const channel = supabase
      .channel('captures-realtime')
      .on(
        'postgres_changes',
        { event: '*', schema: 'public', table: 'captures' },
        (payload) => {
          if (payload.eventType === 'INSERT') {
            setCaptures((prev) => [payload.new as Capture, ...prev]);
          }
          if (payload.eventType === 'UPDATE') {
            setCaptures((prev) =>
              prev.map((c) => (c.id === payload.new.id ? { ...c, ...payload.new } : c))
            );
          }
          if (payload.eventType === 'DELETE') {
            setCaptures((prev) => prev.filter((c) => c.id !== payload.old.id));
          }
        }
      )
      .subscribe();

    return () => {
      supabase.removeChannel(channel);
    };
  }, []);

  return captures;
}

Este hook se encarga de:

  1. Cargar los datos iniciales al montar el componente.
  2. Escuchar cualquier cambio posterior en la tabla.
  3. Actualizar el estado local de forma granular según el tipo de evento.
  4. Limpiar la suscripción al desmontar.

Optimistic UI: la ilusión de velocidad

Para que la app se sienta instantánea, usé actualizaciones optimistas: aplicar el cambio en el estado local antes de que Supabase confirme la operación.

async function addCapture(newCapture: NewCapture) {
  const optimisticItem = { ...newCapture, id: crypto.randomUUID(), pending: true };

  // Actualiza la UI inmediatamente
  setCaptures((prev) => [optimisticItem, ...prev]);

  // Persiste en Supabase
  const { error } = await supabase.from('captures').insert(newCapture);

  if (error) {
    // Revierte si falla
    setCaptures((prev) => prev.filter((c) => c.id !== optimisticItem.id));
  }
}

El evento de Realtime que llega después simplemente actualiza el registro temporal con el ID real del servidor.

Presencia: saber quién está conectado

Supabase Realtime también ofrece Presence, que permite rastrear qué usuarios están activos en un canal en tiempo real. Lo usé para mostrar indicadores de actividad:

const channel = supabase.channel('room', {
  config: { presence: { key: userId } },
});

channel.on('presence', { event: 'sync' }, () => {
  const state = channel.presenceState();
  setActiveUsers(Object.keys(state));
});

channel.subscribe(async (status) => {
  if (status === 'SUBSCRIBED') {
    await channel.track({ user_id: userId, online_at: new Date().toISOString() });
  }
});

Row Level Security: tiempo real seguro

El tiempo real no desactiva RLS. Cada evento que llega a un cliente está filtrado por las políticas de seguridad de la tabla. Esto es crítico: los usuarios solo reciben eventos sobre registros a los que tienen acceso, sin importar cuántos cambios ocurran en la base de datos.

-- Solo recibir eventos de tus propias capturas
CREATE POLICY "Users can see their own captures"
ON captures FOR SELECT
USING (auth.uid() = user_id);

Lecciones aprendidas

Muchas gracias por leer este artículo. Espero que te haya sido útil para entender cómo aprovechar Supabase Realtime en tus propios proyectos.