Codea Bien Logo
Realtime: Apps que se actualizan solas
Supabase

Realtime: Apps que se actualizan solas

Kevin Dávila

supabase realtimewebsocketsreal-time updatesbroadcastpresence

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.