Skip to main content

Overview

Subscriptions allow you to listen to real-time events in your Supabase application. This guide covers all subscription types and patterns.

Database Changes

Listen to All Events

Subscribe to all INSERT, UPDATE, and DELETE operations:
const subscription = supabase
  .channel('schema-db-changes')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'countries'
    },
    (payload) => {
      console.log('Change received!', payload)
    }
  )
  .subscribe()

Listen to INSERT Events

Only receive notifications for new rows:
const subscription = supabase
  .channel('inserts')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'messages'
    },
    (payload) => {
      console.log('New message:', payload.new)
    }
  )
  .subscribe()

Listen to UPDATE Events

Receive notifications when rows are updated:
const subscription = supabase
  .channel('updates')
  .on(
    'postgres_changes',
    {
      event: 'UPDATE',
      schema: 'public',
      table: 'users'
    },
    (payload) => {
      console.log('Updated from:', payload.old)
      console.log('Updated to:', payload.new)
    }
  )
  .subscribe()

Listen to DELETE Events

Receive notifications when rows are deleted:
const subscription = supabase
  .channel('deletes')
  .on(
    'postgres_changes',
    {
      event: 'DELETE',
      schema: 'public',
      table: 'messages'
    },
    (payload) => {
      console.log('Deleted row:', payload.old)
    }
  )
  .subscribe()

Filtering Subscriptions

Filter by Column Value

Only receive changes for specific rows:
const userId = '123'

const subscription = supabase
  .channel('user-messages')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'messages',
      filter: `user_id=eq.${userId}`
    },
    (payload) => {
      console.log('User message change:', payload)
    }
  )
  .subscribe()
filter
string
Filter expression in the format column=op.value.Supported operators:
  • eq - equals
  • neq - not equals
  • lt - less than
  • lte - less than or equal
  • gt - greater than
  • gte - greater than or equal

Multiple Filters

// Only for premium users in a specific room
const subscription = supabase
  .channel('premium-room')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'messages',
      filter: `room_id=eq.${roomId}`
    },
    (payload) => {
      if (payload.new.is_premium) {
        console.log('Premium message:', payload.new)
      }
    }
  )
  .subscribe()

Broadcast Subscriptions

Listen to Broadcasts

Receive ephemeral messages from other clients:
const subscription = supabase
  .channel('room-1')
  .on(
    'broadcast',
    { event: 'cursor-pos' },
    (payload) => {
      console.log('Cursor position:', payload)
    }
  )
  .subscribe()

Send Broadcasts

Send messages to all subscribers:
await subscription.send({
  type: 'broadcast',
  event: 'cursor-pos',
  payload: { x: 100, y: 200, user: 'user-1' }
})

Receive Own Broadcasts

By default, you don’t receive your own broadcasts. Enable this:
const channel = supabase.channel('room-1', {
  config: {
    broadcast: { self: true }
  }
})

channel
  .on('broadcast', { event: 'message' }, (payload) => {
    console.log('Message (including own):', payload)
  })
  .subscribe()

Presence Subscriptions

Track User Presence

Share and sync state across clients:
const channel = supabase.channel('online-users')

channel
  .on('presence', { event: 'sync' }, () => {
    const state = channel.presenceState()
    console.log('Online users:', Object.keys(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-1',
        online_at: new Date().toISOString()
      })
    }
  })

Presence Events

sync
event
Fired when presence state is in sync with the server.
join
event
Fired when a new client joins.
leave
event
Fired when a client leaves.

Update Presence State

Update your presence state:
await channel.track({
  user_id: 'user-1',
  status: 'typing',
  last_active: new Date().toISOString()
})

Untrack Presence

Remove your presence:
await channel.untrack()

Combined Subscriptions

Subscribe to multiple event types on one channel:
const channel = supabase
  .channel('chat-room-1')
  // Database changes
  .on(
    'postgres_changes',
    { event: 'INSERT', schema: 'public', table: 'messages' },
    (payload) => {
      addMessage(payload.new)
    }
  )
  // Typing indicators
  .on(
    'broadcast',
    { event: 'typing' },
    (payload) => {
      showTypingIndicator(payload.user)
    }
  )
  // Online users
  .on(
    'presence',
    { event: 'sync' },
    () => {
      updateOnlineUsers(channel.presenceState())
    }
  )
  .subscribe()

React Patterns

useState Hook

import { useEffect, useState } from 'react'

function Messages() {
  const [messages, setMessages] = useState([])
  
  useEffect(() => {
    const channel = supabase
      .channel('messages')
      .on(
        'postgres_changes',
        { event: 'INSERT', schema: 'public', table: 'messages' },
        (payload) => {
          setMessages((current) => [...current, payload.new])
        }
      )
      .subscribe()
    
    return () => {
      supabase.removeChannel(channel)
    }
  }, [])
  
  return (
    <div>
      {messages.map((msg) => (
        <div key={msg.id}>{msg.text}</div>
      ))}
    </div>
  )
}

useReducer Hook

import { useEffect, useReducer } from 'react'

function messagesReducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return [...state, action.payload]
    case 'UPDATE':
      return state.map((msg) =>
        msg.id === action.payload.id ? action.payload : msg
      )
    case 'DELETE':
      return state.filter((msg) => msg.id !== action.payload.id)
    default:
      return state
  }
}

function Messages() {
  const [messages, dispatch] = useReducer(messagesReducer, [])
  
  useEffect(() => {
    const channel = supabase
      .channel('messages')
      .on(
        'postgres_changes',
        { event: 'INSERT', schema: 'public', table: 'messages' },
        (payload) => dispatch({ type: 'ADD', payload: payload.new })
      )
      .on(
        'postgres_changes',
        { event: 'UPDATE', schema: 'public', table: 'messages' },
        (payload) => dispatch({ type: 'UPDATE', payload: payload.new })
      )
      .on(
        'postgres_changes',
        { event: 'DELETE', schema: 'public', table: 'messages' },
        (payload) => dispatch({ type: 'DELETE', payload: payload.old })
      )
      .subscribe()
    
    return () => supabase.removeChannel(channel)
  }, [])
  
  return <MessageList messages={messages} />
}

Performance Optimization

Debounce Updates

import { debounce } from 'lodash'

const debouncedUpdate = debounce((payload) => {
  updateUI(payload)
}, 300)

supabase
  .channel('updates')
  .on(
    'postgres_changes',
    { event: '*', schema: 'public', table: 'analytics' },
    debouncedUpdate
  )
  .subscribe()

Throttle Updates

import { throttle } from 'lodash'

const throttledUpdate = throttle((payload) => {
  updateUI(payload)
}, 1000)

supabase
  .channel('high-frequency')
  .on(
    'broadcast',
    { event: 'cursor' },
    throttledUpdate
  )
  .subscribe()

Error Handling

Handle Subscription Errors

const channel = supabase
  .channel('messages')
  .on(
    'postgres_changes',
    { event: '*', schema: 'public', table: 'messages' },
    (payload) => {
      try {
        processMessage(payload)
      } catch (error) {
        console.error('Error processing message:', error)
      }
    }
  )
  .subscribe((status, err) => {
    if (status === 'CHANNEL_ERROR') {
      console.error('Subscription error:', err)
      // Implement retry logic
    }
  })

Best Practices

Prevent memory leaks by cleaning up subscriptions:
useEffect(() => {
  const channel = supabase.channel('my-channel').subscribe()
  return () => supabase.removeChannel(channel)
}, [])
Filter at the database level instead of in your app:
filter: `user_id=eq.${userId}`
Always handle subscription errors gracefully.
Use throttling for cursor movements, typing indicators, etc.
Use Row Level Security to control who can subscribe to what data.

Next Steps

Channels

Learn about channel management

Client Libraries

Use Realtime with client SDKs

Build docs developers (and LLMs) love