Supabase proporciona suscripciones de PostgreSQL, funciones en la nube y autenticación directamente, eliminando la necesidad de configuración de backend. Con un entendimiento de su modelo de seguridad a nivel de fila y evitando errores comunes de autenticación, puedes implementar una función en tiempo real en menos de dos horas.
Para quién es esto: Fundadores solitarios que crean productos SaaS, herramientas internas o funciones multijugador que necesitan sincronización en tiempo real sin manejar infraestructura de WebSocket o desplegar un backend separado. Si la falta de SQL en Firebase te molesta y quieres evitar el bloqueo de proveedor, esta es la pila para ti.
Por qué Supabase Real-Time es mejor que crear tu propia solución
La mayoría de los hackers independientes pasan semanas construyendo servidores WebSocket, gestionando conexiones y depurando condiciones de carrera. Supabase ofrece LISTEN/NOTIFY de PostgreSQL envuelto en un cliente de JavaScript que maneja automáticamente la reconexión, la presencia y los canales de difusión.
El motor en tiempo real aprovecha Phoenix Channels bajo el capó (según la documentación oficial de arquitectura de Supabase, 2024). Suscribiéndose a cambios en la base de datos a nivel de tabla o fila, Supabase transmite inserciones, actualizaciones y eliminaciones a tu cliente en milisegundos. No se necesita polling. No se requieren endpoints API personalizados.
Esto es lo que obtienes:
- Suscripciones a la base de datos: Escucha
INSERT,UPDATE,DELETEen cualquier tabla. - Presencia: Rastrea quién está en línea en una sala o documento.
- Difusión: Envía mensajes efímeros (por ejemplo, posiciones del cursor, indicadores de escritura).
- Funciones en la nube: Despliega funciones de TypeScript sin servidor a nivel global.
El inconveniente: las políticas de seguridad a nivel de fila (RLS) dictan a qué puede suscribirse cada usuario. Si configuras mal RLS, tus suscripciones en tiempo real fallan silenciosamente. El problema casi siempre se remonta a RLS.
Configura tu proyecto Supabase y habilita Real-Time
Crea un nuevo proyecto en supabase.com. En menos de 60 segundos, recibirás una base de datos PostgreSQL, una API REST y un servidor en tiempo real. Elige una región cerca de tus usuarios: la latencia es crucial para el tiempo real.
Instala el cliente de JavaScript:
npm install @supabase/supabase-js
Inicializa en tu aplicación:
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = 'https://your-project.supabase.co'
const supabaseKey = 'your-anon-key'
const supabase = createClient(supabaseUrl, supabaseKey)
Habilita el tiempo real en tu tabla. Por defecto, Supabase desactiva la replicación en tiempo real para ahorrar recursos. Ve a Base de datos → Replicación en el panel, localiza tu tabla y activa el tiempo real. Si omites esto, tus suscripciones se conectarán pero nunca recibirán eventos.
Crea una tabla simple de messages a través del Editor SQL:
CREATE TABLE messages (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ DEFAULT NOW(),
user_id UUID REFERENCES auth.users(id),
content TEXT NOT NULL,
room_id TEXT NOT NULL
);
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
Habilita RLS de inmediato. Nunca ejecutes una tabla de Supabase en producción sin ello.
Escribe políticas de seguridad a nivel de fila que no rompan
Aquí está el detalle: la mayoría de los tutoriales se detienen aquí, pero tu aplicación fallará sin políticas RLS adecuadas. Las suscripciones en tiempo real respetan las políticas RLS. Si un usuario no puede SELECT una fila, no recibirá actualizaciones en tiempo real para ella.
Crea una política que permita a los usuarios leer mensajes en su sala:
CREATE POLICY "Users can read messages in their rooms"
ON messages FOR SELECT
USING (auth.uid() IS NOT NULL);
Para inserciones:
CREATE POLICY "Users can insert their own messages"
ON messages FOR INSERT
WITH CHECK (auth.uid() = user_id);
Prueba esto en el Editor SQL ejecutando:
SELECT * FROM messages;
Si estás desconectado (sin JWT), recibirás cero filas, incluso si la tabla tiene datos. Las suscripciones se comportan de la misma manera.
Error común: Crear una política con TO authenticated pero olvidar que authenticated se aplica solo a usuarios con una sesión válida. Al probar con la clave anónima y sin iniciar sesión, las suscripciones se conectan pero nunca emiten eventos. Usa auth.uid() IS NOT NULL o establece explícitamente políticas TO anon para datos públicos.
Suscríbete a cambios en la base de datos en tiempo real
Ahora suscríbete a inserciones en la tabla messages:
const channel = supabase
.channel('room-1')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: 'room_id=eq.room-1'
},
(payload) => {
console.log('Nuevo mensaje:', payload.new)
// Actualiza tu UI aquí
}
)
.subscribe()
El parámetro filter utiliza la sintaxis de PostgREST: columna=operador.valor. Filtra por cualquier columna. Esto es del lado del servidor, por lo que no estás transmitiendo cada fila y filtrando del lado del cliente.
Para cancelar la suscripción:
supabase.removeChannel(channel)
Vale la pena mencionar: las suscripciones no reproducen eventos perdidos. Si un usuario se desconecta durante 10 segundos y se insertan cinco mensajes, no los verá al reconectarse. Obtén las filas más recientes al reconectar y fusiona con el estado local.
Aquí está el patrón utilizado:
async function initializeRoom(roomId) {
// Obtener mensajes existentes
const { data } = await supabase
.from('messages')
.select('*')
.eq('room_id', roomId)
.order('created_at', { ascending: true })
setMessages(data)
// Suscribirse a nuevos mensajes
const channel = supabase
.channel(roomId)
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'messages', filter: `room_id=eq.${roomId}` },
(payload) => {
setMessages(prev => [...prev, payload.new])
}
)
.subscribe()
return () => supabase.removeChannel(channel)
}
Obtén primero, luego suscríbete. No al revés.
Usa Presencia para rastrear usuarios en línea
La presencia sincroniza un estado JSON arbitrario entre todos los clientes en un canal. Perfecto para "quién está en línea" o "quién está viendo este documento."
const channel = supabase.channel('room-1')
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('Usuarios en línea:', state)
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('Usuario se unió:', newPresences)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('Usuario salió:', leftPresences)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
user_id: user.id,
online_at: new Date().toISOString()
})
}
})
Llama a channel.track() después de que se confirme la suscripción. El objeto pasado se transmite a todos los suscriptores. Supabase gestiona los latidos y elimina a los usuarios desconectados.
Limitación: El estado de presencia es temporal. Si todos los usuarios se desconectan, el estado se restablece. Úsalo solo para el estado de UI transitorio, no para datos persistentes como posiciones de cursor o indicadores de escritura.
Implementa un chat en tiempo real en menos de 100 líneas
Aquí tienes un ejemplo funcional en React que combina todo:
import { useEffect, useState } from 'react'
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('YOUR_URL', 'YOUR_ANON_KEY')
export default function Chat({ roomId, user }) {
const [messages, setMessages] = useState([])
const [newMessage, setNewMessage] = useState('')
useEffect(() => {
// Obtener mensajes existentes
supabase
.from('messages')
.select('*')
.eq('room_id', roomId)
.order('created_at', { ascending: true })
.then(({ data }) => setMessages(data || []))
// Suscribirse a nuevos mensajes
const channel = supabase
.channel(roomId)
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'messages', filter: `room_id=eq.${roomId}` },
(payload) => setMessages(prev => [...prev, payload.new])
)
.subscribe()
return () => supabase.removeChannel(channel)
}, [roomId])
const sendMessage = async () => {
if (!newMessage.trim()) return
await supabase.from('messages').insert({
content: newMessage,
room_id: roomId,
user_id: user.id
})
setNewMessage('')
}
return (
<div>
<div>
{messages.map(msg => (
<div key={msg.id}>{msg.content}</div>
))}
</div>
<input
value={newMessage}
onChange={e => setNewMessage(e.target.value)}
onKeyPress={e => e.key === 'Enter' && sendMessage()}
/>
</div>
)
}
Esto maneja la obtención, la suscripción y la inserción. Las políticas RLS aseguran que los usuarios solo vean los mensajes permitidos. Sin código de backend. Sin gestión de WebSocket.
Lo que nadie te dice sobre Supabase Real-Time
1. Las suscripciones no reintentan inserciones fallidas. Si un cliente envía un INSERT que viola RLS, no aparece ningún error en la devolución de llamada de la suscripción. La inserción falla silenciosamente. Siempre gestiona los errores de inserción en tu llamada supabase.from().insert().
2. Se te cobra por minutos de conexión. Supabase cuenta las conexiones en tiempo real activas. Si 100 usuarios se conectan durante una hora, eso son 100 horas de conexión. El nivel gratuito incluye 500,000 minutos de conexión mensuales (según los precios de Supabase, 2026), aproximadamente 347 usuarios conectados 24/7. Más allá de eso, cuesta $0.00002 por minuto. Escala con la concurrencia, no con el uso, pero no es costoso.
3. Los canales de difusión no garantizan la entrega. Usar channel.send() para eventos como movimientos del cursor puede causar pérdidas de mensajes bajo carga. Para eventos críticos, escribe en la base de datos y suscríbete a los cambios.
4. Las funciones en la nube tienen inicios en frío. Las funciones se ejecutan en Deno Deploy. La invocación inicial después de estar inactivo puede tardar de 200 a 500 ms. No es un problema para tareas asíncronas, pero no es adecuado para lógica en tiempo real sensible a la latencia. Mantén eso del lado del cliente o en disparadores de base de datos.
5. Los filtros no se aplican a la presencia o la difusión. El parámetro filter funciona solo para eventos postgres_changes. Al suscribirse a presencia o difusión, todos los eventos van a cada cliente en el canal. Filtra del lado del cliente.
Errores comunes que rompen el tiempo real
Olvidar habilitar la replicación. Las suscripciones que se conectan pero nunca se activan a menudo son el resultado de olvidar habilitar la replicación. Siempre verifica Base de datos → Replicación en el panel.
Configurar RLS después de comenzar a probar. Las políticas RLS se evalúan en el momento de la consulta. Crear una suscripción antes de habilitar RLS puede funcionar. Habilita RLS y la suscripción puede dejar de recibir eventos. Elimina y recrea la suscripción después de cambiar las políticas.
No gestionar los cambios en el estado de autenticación. El inicio o cierre de sesión de un usuario cambia su JWT, alterando lo que pueden SELECT. Las suscripciones no se actualizan automáticamente. Cancela la suscripción y vuelve a suscribirte en los cambios de estado de autenticación:
useEffect(() => {
const { data: authListener } = supabase.auth.onAuthStateChange(() => {
// Re-inicializa las suscripciones
})
return () => authListener.subscription.unsubscribe()
}, [])
Usar eventos INSERT para presencia. No escribas el estado de presencia en una tabla y suscribas a inserciones. La API de Presencia es adecuada para el estado efímero y maneja la limpieza automáticamente.
Preguntas Frecuentes
¿Cuántas conexiones en tiempo real concurrentes puede manejar Supabase?
La capa gratuita admite hasta 200 conexiones concurrentes. Los planes de pago comienzan en 500 y escalan a miles según tu plan (según documentación de Supabase, 2026). ¿Necesitas más? Contacta a su equipo empresarial. Para la mayoría de los indie hackers, 500 son suficientes para validar el ajuste producto-mercado.
¿Puede Supabase en tiempo real funcionar con React Native o aplicaciones móviles?
Sí. El cliente @supabase/supabase-js funciona en React Native, Expo, Flutter (a través de supabase-flutter) y Swift. Las conexiones WebSocket operan de manera similar. Asegúrate de que las aplicaciones móviles manejen correctamente el fondo: las suscripciones se desconectan cuando la aplicación está en segundo plano y deben reconectarse al reanudar.
¿Funciona Supabase en tiempo real con funciones sin servidor o rutas API de Next.js?
Las suscripciones en tiempo real son estrictamente del lado del cliente. No es posible suscribirse a cambios en la base de datos desde una función sin servidor, ya que la función termina después de la respuesta. Usa disparadores de base de datos o pg_notify para la lógica en tiempo real del lado del servidor. Con Next.js, suscríbete desde el navegador, no desde las rutas API.
¿Qué pasa si se supera el límite de conexiones?
Las nuevas conexiones fallarán. Las conexiones existentes permanecen activas. Espera errores del cliente como channel timeout o subscription failed. Actualiza tu plan u optimiza el uso de conexiones: evita crear nuevas suscripciones por componente; comparte una a través de tu aplicación.
Siguiente paso: Construye tu primera función
Elige una función en tiempo real: comentarios en vivo, notificaciones o edición colaborativa. Crea una tabla, habilita la replicación, escribe políticas de RLS y suscríbete desde tu frontend. Lánzalo a un solo usuario y monitorea el tráfico de WebSocket en la pestaña de red del navegador. La experiencia práctica supera a innumerables tutoriales.
Supabase en tiempo real no es magia: es replicación de PostgreSQL a través de una API bien diseñada. Comprende RLS, prueba tus políticas y podrás lanzar funciones en horas que antes tomaban semanas. Para más información sobre cómo lanzar tu SaaS, consulta Por qué falla el lanzamiento de tu SaaS: lecciones reales de fundadores y explora ideas innovadoras con Mejores ideas de Micro-SaaS para Indie Hackers en 2026.
Nota editorial: Este artículo fue elaborado con asistencia de IA y revisado por Javier Valencia. A lo largo del texto se distinguen los hechos verificados de la opinión editorial. Las fuentes externas enlazadas son independientes de NewsTide.