Codea Bien Logo
Realtime: Apps That Update Themselves
Supabase

Realtime: Apps That Update Themselves

Kevin Dávila

Kevin Dávila

supabase realtimewebsocketsreal-time updatesbroadcastpresence

20 lines. That's what the real-time chat cost. No custom WebSockets, no Redis, no middleware server to babysit at 3 AM.

When I saw it working, I finally got why people can't stop talking about Supabase Realtime. Here you'll build features that update themselves: from listening to database changes to knowing who's online.

What is Supabase Realtime

Supabase Realtime is a service that allows receiving real-time updates from your database. It uses WebSockets under the hood and offers three main functionalities:

  • Database Changes: Listen to table changes

  • Broadcast: Messages between clients without going through DB

  • Presence: Online/offline user state

Listen to database changes

Subscribe to a table

// src/realtime/tasks.ts
import { supabase } from '../lib/supabase'

// Listen to new inserts in tasks table
const channel = supabase
  .channel('tasks-changes')
  .on(
    'postgres_changes',
    {
      event: '*', // INSERT, UPDATE, DELETE, or '*'
      schema: 'public',
      table: 'tasks'
    },
    (payload) => {
      console.log('Change received!', payload)
      // payload.new - new values
      // payload.old - previous values
      // payload.eventType - 'INSERT', 'UPDATE', 'DELETE'
    }
  )
  .subscribe()

// To disconnect
// supabase.removeChannel(channel)

Filter specific changes

// 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()

Multiple subscriptions in one channel

// 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 allows sending messages between clients without saving to the database. Ideal for chat, notifications, and temporary actions.

Send messages

// src/realtime/broadcast.ts
const channel = supabase.channel('room-1')

// Send message
await channel.send({
  type: 'broadcast',
  event: 'chat-message',
  payload: {
    user: 'Kevin',
    message: 'Hello!',
    timestamp: new Date().toISOString()
  }
})

Receive messages

// 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: 'Hello!', timestamp: '...' }
    }
  )
  .subscribe()

Example: Real-time chat

// 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)
  }
}

// Usage
const chat = new RealtimeChat('general')
chat.join('user-123', (msg) => {
  console.log(`${msg.userId}: ${msg.text}`)
})

await chat.sendMessage('user-123', 'Hello everyone!')

Presence

Presence allows knowing who is online in a room. Ideal for "active users" indicators, "who's viewing this", etc.

Track presence

// src/realtime/presence.ts
const channel = supabase.channel('online-users')

// Sync presence
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()
      })
    }
  })

Example: Online users indicator

// 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} user${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)
  }
}

Complete example: Real-time dashboard

// src/dashboard.ts
import { supabase } from './lib/supabase'

export class RealtimeDashboard {
  private channels: ReturnType<typeof supabase.channel>[] = []

  start() {
    // Listen to new orders
    const ordersChannel = supabase
      .channel('orders')
      .on(
        'postgres_changes',
        { event: 'INSERT', schema: 'public', table: 'orders' },
        (payload) => {
          this.addOrderToUI(payload.new)
          this.updateStats()
        }
      )
      .subscribe()

    // Listen to inventory updates
    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) {
    // Add order to UI
    console.log('New order:', order)
  }

  private updateProductInUI(product: any) {
    // Update product in UI
    console.log('Product updated:', product)
  }

  private updateStats() {
    // Update stats
    console.log('Updating stats...')
  }

  stop() {
    this.channels.forEach(ch => supabase.removeChannel(ch))
  }
}

Realtime configuration

In the Supabase dashboard, go to Database > Replication to enable Realtime on specific tables.

Enable from SQL

-- Enable Realtime on a table
ALTER PUBLICATION supabase_realtime ADD TABLE tasks;

Enable for multiple tables

ALTER PUBLICATION supabase_realtime ADD TABLE tasks, orders, messages;

Realtime limits

  • Concurrent connections: 200 per project (free tier)

  • Messages: Unlimited

  • Channels: Unlimited

  • Presence: 1000 keys per channel

Common errors

"Channel not subscribing"

  • Verify the table has Realtime enabled

  • Check RLS policies

No events received

  • Table is not in Realtime publication

  • RLS policies are blocking changes

Connection disconnects

  • Supabase reconnects automatically

  • Check your internet connection

What's next

In the next article we cover Edge Functions: how to execute serverless logic at the edge. We'll see how to use them for webhooks, payment processing, and more.