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:
- Feeds de actividad compartidos
- Tablas o listas colaborativas
- Notificaciones en vivo
- Dashboards que se actualizan solos
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:
- Cargar los datos iniciales al montar el componente.
- Escuchar cualquier cambio posterior en la tabla.
- Actualizar el estado local de forma granular según el tipo de evento.
- 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
- No hagas polling: Supabase Realtime es más eficiente y simple.
- Granularidad en los handlers: manejar
INSERT,UPDATEyDELETEpor separado evita re-renders innecesarios. - Siempre limpia tus canales: el
removeChannelen el cleanup deluseEffectes obligatorio para evitar memory leaks. - Optimistic UI + Realtime: la combinación perfecta para una experiencia fluida.
- RLS es tu primera línea de defensa: configúralo antes de activar el tiempo real.
Muchas gracias por leer este artículo. Espero que te haya sido útil para entender cómo aprovechar Supabase Realtime en tus propios proyectos.