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 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
Fired when presence state is in sync with the server.
Fired when a new client joins.
Unique identifier for the presence.
Array of new presence states.
Fired when a client leaves.
Unique identifier for the presence.
Array of left presence states.
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:
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} />
}
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)
}, [])
Use filters to reduce traffic
Filter at the database level instead of in your app:filter: `user_id=eq.${userId}`
Always handle subscription errors gracefully.
Throttle high-frequency events
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