Realtime: Apps que se actualizan solas
Kevin Dávila
20 líneas. Eso costó el chat en tiempo real. Sin WebSockets custom, sin Redis, sin un servidor intermedio que mantener a las 3 de la mañana.
Cuando lo vi funcionar entendí por qué la gente no para de hablar de Supabase Realtime. Aquí vas a construir features que se actualizan solas: desde escuchar cambios en tu base de datos hasta saber quién está online.
Qué es Supabase Realtime
Supabase Realtime es un servicio que permite recibir actualizaciones en tiempo real de tu base de datos. Usa WebSockets bajo el capó y ofrece tres funcionalidades principales:
Database Changes: Escuchar cambios en tablas
Broadcast: Mensajes entre clientes sin pasar por la DB
Presence: Estado online/offline de usuarios
Escuchar cambios en la base de datos
Suscripción a una tabla
// src/realtime/tasks.ts
import { supabase } from '../lib/supabase'
// Escuchar nuevos inserts en la tabla tasks
const channel = supabase
.channel('tasks-changes')
.on(
'postgres_changes',
{
event: '*', // INSERT, UPDATE, DELETE, o '*'
schema: 'public',
table: 'tasks'
},
(payload) => {
console.log('Change received!', payload)
// payload.new - nuevos valores
// payload.old - valores anteriores
// payload.eventType - 'INSERT', 'UPDATE', 'DELETE'
}
)
.subscribe()
// Para desconectar
// supabase.removeChannel(channel)Filtrar cambios específicos
// src/realtime/filtered.ts
const channel = supabase
.channel('my-tasks')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'tasks',
filter: 'user_id=eq.123e4567-e89b-12d3-a456-426614174000'
},
(payload) => {
console.log('New task for user:', payload.new)
}
)
.subscribe()Múltiples suscripciones en un canal
// src/realtime/multiple.ts
const channel = supabase
.channel('db-changes')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'tasks' },
(payload) => console.log('New task:', payload.new)
)
.on(
'postgres_changes',
{ event: 'UPDATE', schema: 'public', table: 'tasks' },
(payload) => console.log('Task updated:', payload.new)
)
.on(
'postgres_changes',
{ event: 'DELETE', schema: 'public', table: 'tasks' },
(payload) => console.log('Task deleted:', payload.old)
)
.subscribe()Broadcast
Broadcast permite enviar mensajes entre clientes sin guardar en la base de datos. Ideal para chat, notificaciones, y acciones temporales.
Enviar mensajes
// src/realtime/broadcast.ts
const channel = supabase.channel('room-1')
// Enviar mensaje
await channel.send({
type: 'broadcast',
event: 'chat-message',
payload: {
user: 'Kevin',
message: 'Hola!',
timestamp: new Date().toISOString()
}
})Recibir mensajes
// src/realtime/subscribe.ts
const channel = supabase
.channel('room-1')
.on(
'broadcast',
{ event: 'chat-message' },
(payload) => {
console.log('Message received:', payload.payload)
// { user: 'Kevin', message: 'Hola!', timestamp: '...' }
}
)
.subscribe()Ejemplo: Chat en tiempo real
// src/chat.ts
import { supabase } from './lib/supabase'
export class RealtimeChat {
private channel: ReturnType<typeof supabase.channel>
constructor(roomId: string) {
this.channel = supabase.channel(`chat-${roomId}`)
}
join(userId: string, onMessage: (msg: any) => void) {
this.channel
.on('broadcast', { event: 'message' }, ({ payload }) => {
onMessage(payload)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
console.log('Joined room')
}
})
}
async sendMessage(userId: string, text: string) {
await this.channel.send({
type: 'broadcast',
event: 'message',
payload: {
userId,
text,
timestamp: new Date().toISOString()
}
})
}
leave() {
supabase.removeChannel(this.channel)
}
}
// Uso
const chat = new RealtimeChat('general')
chat.join('user-123', (msg) => {
console.log(`${msg.userId}: ${msg.text}`)
})
await chat.sendMessage('user-123', 'Hola a todos!')Presence
Presence permite saber quién está online en una sala. Ideal para indicadores de "usuarios activos", "quién está viendo esto", etc.
Tracking de presencia
// src/realtime/presence.ts
const channel = supabase.channel('online-users')
// Sync presencia
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('Online users:', Object.keys(state).length)
console.log('Users:', state)
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('User joined:', key, newPresences)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('User left:', key, leftPresences)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
user_id: 'user-123',
username: 'kevin',
online_at: new Date().toISOString()
})
}
})Ejemplo: Indicador de usuarios online
// src/components/OnlineUsers.ts
import { supabase } from '../lib/supabase'
export class OnlineUsersIndicator {
private channel: ReturnType<typeof supabase.channel>
private container: HTMLElement
constructor(containerId: string) {
this.container = document.getElementById(containerId)!
this.channel = supabase.channel('page-presence')
}
start(userId: string) {
this.channel
.on('presence', { event: 'sync' }, () => {
const state = this.channel.presenceState()
const count = Object.keys(state).length
this.container.textContent = `${count} usuario${count !== 1 ? 's' : ''} online`
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await this.channel.track({
user_id: userId,
page: window.location.pathname
})
}
})
}
stop() {
supabase.removeChannel(this.channel)
}
}Ejemplo completo: Dashboard en tiempo real
// src/dashboard.ts
import { supabase } from './lib/supabase'
export class RealtimeDashboard {
private channels: ReturnType<typeof supabase.channel>[] = []
start() {
// Escuchar nuevas órdenes
const ordersChannel = supabase
.channel('orders')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'orders' },
(payload) => {
this.addOrderToUI(payload.new)
this.updateStats()
}
)
.subscribe()
// Escuchar actualizaciones de inventario
const inventoryChannel = supabase
.channel('inventory')
.on(
'postgres_changes',
{ event: 'UPDATE', schema: 'public', table: 'products' },
(payload) => {
this.updateProductInUI(payload.new)
}
)
.subscribe()
this.channels.push(ordersChannel, inventoryChannel)
}
private addOrderToUI(order: any) {
// Agregar orden a la UI
console.log('New order:', order)
}
private updateProductInUI(product: any) {
// Actualizar producto en la UI
console.log('Product updated:', product)
}
private updateStats() {
// Actualizar estadísticas
console.log('Updating stats...')
}
stop() {
this.channels.forEach(ch => supabase.removeChannel(ch))
}
}Configuración de Realtime
En el dashboard de Supabase, ve a Database > Replication para habilitar Realtime en tablas específicas.
Habilitar desde SQL
-- Habilitar Realtime en una tabla
ALTER PUBLICATION supabase_realtime ADD TABLE tasks;Habilitar para múltiples tablas
ALTER PUBLICATION supabase_realtime ADD TABLE tasks, orders, messages;Límites de Realtime
Concurrent connections: 200 por proyecto (free tier)
Messages: Ilimitados
Channels: Ilimitados
Presence: 1000 keys por canal
Errores comunes
"Channel not subscribing"
Verifica que la tabla tiene Realtime habilitado
Revisa las políticas de RLS
No se reciben eventos
La tabla no está en la publicación de Realtime
Las políticas de RLS están bloqueando los cambios
Conexión se desconecta
Supabase reconecta automáticamente
Verifica tu conexión a internet
Qué sigue
En el próximo artículo vamos con Edge Functions: cómo ejecutar lógica serverless en el borde. Vamos a ver cómo usarlas para webhooks, procesamiento de pagos, y más.