Realtime en Supabase: Canales y Presencia
Supabase Realtime te permite enviar y recibir datos al instante entre clientes conectados. Funciona sobre WebSockets (conexiones persistentes entre el navegador y el servidor que permiten comunicación bidireccional).
Realtime tiene tres funcionalidades principales:
| Funcionalidad | Descripción | Uso típico |
|---|---|---|
| Postgres Changes | Escuchar cambios en tablas de la base de datos | Actualizar UI cuando otros usuarios modifican datos |
| Broadcast | Enviar mensajes entre clientes conectados | Chat, notificaciones, cursor compartido |
| Presence | Rastrear qué usuarios están online | Indicadores de "en línea", lista de participantes |
Canales (Channels)
Un canal es un tema (topic) al que los clientes se suscriben para enviar y recibir mensajes. Piensa en un canal cómo una sala de chat -- todos los que están en la misma sala reciben los mensajes.
// Crear un canal
const canal = supabase.channel('mi-canal')
// Suscribirse al canal
canal.subscribe((status) => {
if (status === 'SUBSCRIBED') {
console.log('Conectado al canal')
}
})Puedes crear tantos canales como necesites. Cada canal es independiente:
// Canal para chat general
const chatGeneral = supabase.channel('chat-general')
// Canal para un proyecto especifico
const proyecto = supabase.channel('proyecto-123')
// Canal para notificaciones del usuario
const notificaciones = supabase.channel(`notificaciones-${userId}`)Desuscribirse de un canal
// Desuscribirse de un canal especifico
await supabase.removeChannel(canal)
// Desuscribirse de todos los canales
await supabase.removeAllChannels()Broadcast: mensajes entre clientes
Broadcast te permite enviar mensajes arbitrarios a todos los clientes conectados al mismo canal. Los mensajes no se guardan en la base de datos -- son efímeros.
Enviar un mensaje
const canal = supabase.channel('chat-sala-1')
canal.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// Enviar mensaje a todos los clientes en este canal
await canal.send({
type: 'broadcast',
event: 'nuevo-mensaje',
payload: {
usuario: 'Juan',
texto: 'Hola a todos',
timestamp: new Date().toISOString()
}
})
}
})Recibir mensajes
const canal = supabase.channel('chat-sala-1')
canal
.on('broadcast', { event: 'nuevo-mensaje' }, (payload) => {
console.log('Mensaje recibido:', payload.payload)
// { usuario: 'Juan', texto: 'Hola a todos', timestamp: '...' }
})
.subscribe()Ejemplo: posición del cursor compartida
Un caso clásico de broadcast -- mostrar dónde está el cursor de otros usuarios en un documento colaborativo:
'use client'
import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
interface CursorPosition {
userId: string
x: number
y: number
}
export function CursorTracker({ documentId, userId }: { documentId: string; userId: string }) {
const [cursors, setCursors] = useState<Map<string, CursorPosition>>(new Map())
const supabase = createClient()
useEffect(() => {
const canal = supabase.channel(`doc-${documentId}`)
// Escuchar posiciones de otros cursores
canal
.on('broadcast', { event: 'cursor-move' }, ({ payload }) => {
const pos = payload as CursorPosition
if (pos.userId !== userId) {
setCursors((prev) => new Map(prev).set(pos.userId, pos))
}
})
.subscribe()
// Enviar mi posicion del cursor
function handleMouseMove(e: MouseEvent) {
canal.send({
type: 'broadcast',
event: 'cursor-move',
payload: { userId, x: e.clientX, y: e.clientY }
})
}
window.addEventListener('mousemove', handleMouseMove)
return () => {
window.removeEventListener('mousemove', handleMouseMove)
supabase.removeChannel(canal)
}
}, [documentId, userId, supabase])
return (
<>
{Array.from(cursors.entries()).map(([id, pos]) => (
<div
key={id}
className="fixed w-4 h-4 bg-blue-500 rounded-full pointer-events-none"
style={{ left: pos.x, top: pos.y, transform: 'translate(-50%, -50%)' }}
/>
))}
</>
)
}Broadcast no persiste
Los mensajes de broadcast no se guardan en ninguna parte. Si un cliente no está conectado al canal cuando se envía un mensaje, no lo recibe. Si necesitas persistencia, usa Postgres Changes (que escucha cambios en tablas reales).
Presence: rastrear usuarios online
Presence te permite saber qué usuarios están conectados a un canal en este momento. Cada usuario puede compartir un estado (un objeto con datos arbitrarios) que se sincroniza automáticamente.
Compartir tu estado
const canal = supabase.channel('sala-1')
canal.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// Compartir mi presencia
await canal.track({
user_id: userId,
nombre: 'Juan',
online_at: new Date().toISOString()
})
}
})Escuchar cambios de presencia
const canal = supabase.channel('sala-1')
canal
.on('presence', { event: 'sync' }, () => {
// Se ejecuta cada vez que cambia el estado de presencia
const state = canal.presenceState()
console.log('Usuarios online:', state)
// { "key-1": [{ user_id: "...", nombre: "Juan", ... }], ... }
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('Se conecto:', newPresences)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('Se desconecto:', leftPresences)
})
.subscribe()Ejemplo: lista de usuarios online
'use client'
import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
interface UserPresence {
user_id: string
nombre: string
online_at: string
}
export function OnlineUsers({ roomId, currentUser }: {
roomId: string
currentUser: { id: string; nombre: string }
}) {
const [users, setUsers] = useState<UserPresence[]>([])
const supabase = createClient()
useEffect(() => {
const canal = supabase.channel(`sala-${roomId}`)
canal
.on('presence', { event: 'sync' }, () => {
const state = canal.presenceState<UserPresence>()
// Convertir el objeto de presencia a un array plano
const onlineUsers = Object.values(state).flat()
setUsers(onlineUsers)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await canal.track({
user_id: currentUser.id,
nombre: currentUser.nombre,
online_at: new Date().toISOString()
})
}
})
return () => {
supabase.removeChannel(canal)
}
}, [roomId, currentUser, supabase])
return (
<div className="p-4 border border-zinc-700 rounded">
<h3 className="text-sm font-semibold text-zinc-400 mb-2">
En linea ({users.length})
</h3>
<ul className="space-y-1">
{users.map((user) => (
<li key={user.user_id} className="flex items-center gap-2">
<span className="w-2 h-2 bg-green-500 rounded-full" />
<span className="text-sm text-zinc-300">
{user.nombre}
{user.user_id === currentUser.id && ' (tu)'}
</span>
</li>
))}
</ul>
</div>
)
}Presence se limpia automáticamente
Cuando un usuario cierra la pestaña o pierde conexión, Supabase detecta la desconexión y elimina su presencia del canal. No necesitas limpiar manualmente.
Habilitar Realtime en una tabla
Para escuchar cambios en tu base de datos (INSERT, UPDATE, DELETE) necesitas habilitar Realtime en las tablas que quieras monitorear.
Desde el dashboard
- Ve a Database > Replication en el dashboard
- En la sección Supabase Realtime, activa la tabla que quieres monitorear
- Selecciona qué eventos escuchar (INSERT, UPDATE, DELETE)
Con SQL
-- Habilitar realtime en la tabla 'mensajes'
ALTER PUBLICATION supabase_realtime ADD TABLE mensajes;
-- Verificar que tablas tienen realtime habilitado
SELECT * FROM pg_publication_tables
WHERE pubname = 'supabase_realtime';Para deshabilitar:
ALTER PUBLICATION supabase_realtime DROP TABLE mensajes;Realtime y RLS
Las suscripciones a cambios en la base de datos respetan las políticas RLS. Si un usuario no tiene permiso para leer una fila, no recibe la notificación cuando esa fila cambia. Asegúrate de que tus políticas de SELECT permitan a los usuarios relevantes ver los datos.
Casos de uso
Chat en tiempo real
- Broadcast para mensajes efímeros (indicador de "escribiendo...")
- Postgres Changes para mensajes persistentes (se guardan en una tabla
mensajes) - Presence para mostrar quién está en el chat
Notificaciones
- Postgres Changes en una tabla
notificacionespara enviar notificaciones persistentes - Broadcast para notificaciones efimeras (como "nuevo mensaje en el chat")
Edición colaborativa
- Broadcast para compartir cambios de cursor y selección en tiempo real
- Postgres Changes para persistir los cambios del documento
- Presence para mostrar quién está editando
Dashboard en tiempo real
- Postgres Changes para actualizar métricas cuando cambian los datos
- Útil para dashboards de ventas, monitoreo, o cualquier dato que cambie frecuentemente
Juegos multijugador
- Broadcast para posiciones de jugadores y acciones rápidas
- Presence para lobby y lista de jugadores
- Postgres Changes para estado del juego que necesita persistencia
Límites y consideraciones
| Límite | Free tier | Pro tier |
|---|---|---|
| Conexiones simultáneas | 200 | 500 (escalable) |
| Mensajes por segundo | 100 | 500+ |
| Tamaño del payload | 1MB | 1MB |
| Canales por conexión | Sin límite | Sin límite |
- Broadcast es más rápido que Postgres Changes porque no pasa por la base de datos
- Presence tiene un overhead mayor -- no lo uses para datos que cambian muy frecuentemente (como posiciones de cursor con 60fps)
- Para alta frecuencia de actualizaciones, usa Broadcast con throttling (limitar la frecuencia de envío)
Resumen
- Los canales (channels) son el punto de entrada para toda la funcionalidad Realtime
- Broadcast envia mensajes efímeros entre clientes -- no se guardan en la base de datos
- Presence rastrea qué usuarios están online y su estado actual
- Postgres Changes escucha cambios en tablas de la base de datos (requiere habilitar replication)
- Los cambios de base de datos respetan las políticas RLS
- Presence se limpia automáticamente cuando el usuario se desconecta
- Siempre llama a
removeChannel()oremoveAllChannels()al desmontar componentes