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:

FuncionalidadDescripciónUso típico
Postgres ChangesEscuchar cambios en tablas de la base de datosActualizar UI cuando otros usuarios modifican datos
BroadcastEnviar mensajes entre clientes conectadosChat, notificaciones, cursor compartido
PresenceRastrear qué usuarios están onlineIndicadores 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.

typescript
// 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:

typescript
// 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

typescript
// 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

typescript
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

typescript
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:

tsx
'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

typescript
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

typescript
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

tsx
'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

  1. Ve a Database > Replication en el dashboard
  2. En la sección Supabase Realtime, activa la tabla que quieres monitorear
  3. Selecciona qué eventos escuchar (INSERT, UPDATE, DELETE)

Con SQL

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:

sql
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 notificaciones para 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ímiteFree tierPro tier
Conexiones simultáneas200500 (escalable)
Mensajes por segundo100500+
Tamaño del payload1MB1MB
Canales por conexiónSin límiteSin 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() o removeAllChannels() al desmontar componentes