Overview
Channels are the core concept in Supabase Realtime. They provide isolated communication streams for different features of your application.
Creating Channels
Basic Channel
Create a channel with a unique name:
const channel = supabase . channel ( 'room-1' )
Unique identifier for the channel. Can include letters, numbers, and hyphens.
Channel with Options
Configure channel behavior:
const channel = supabase . channel ( 'room-1' , {
config: {
broadcast: { self: true },
presence: { key: 'user-id' }
}
})
Broadcast configuration. Show broadcast properties
Whether to receive your own broadcast messages.
Request acknowledgment for broadcast messages.
Presence configuration. Custom key to use for presence state (defaults to random UUID).
Subscribing to Channels
Subscribe
Activate the channel connection:
const channel = supabase
. channel ( 'room-1' )
. subscribe (( status ) => {
if ( status === 'SUBSCRIBED' ) {
console . log ( 'Channel is ready!' )
}
})
Subscribe with Error Handling
const channel = supabase
. channel ( 'room-1' )
. subscribe (( status , err ) => {
switch ( status ) {
case 'SUBSCRIBED' :
console . log ( 'Successfully subscribed to channel' )
break
case 'TIMED_OUT' :
console . error ( 'Connection timed out' )
break
case 'CLOSED' :
console . log ( 'Channel closed' )
break
case 'CHANNEL_ERROR' :
console . error ( 'Channel error:' , err )
break
}
})
Channel States
Connection States
Channel is connected and ready.
Connection attempt timed out.
An error occurred with the channel.
Check Channel State
const state = channel . state
console . log ( 'Channel state:' , state )
Unsubscribing
Remove Single Channel
await supabase . removeChannel ( channel )
Remove All Channels
await supabase . removeAllChannels ()
Unsubscribe in React
import { useEffect } from 'react'
function MyComponent () {
useEffect (() => {
const channel = supabase
. channel ( 'room-1' )
. subscribe ()
// Cleanup on component unmount
return () => {
supabase . removeChannel ( channel )
}
}, [])
return < div > My Component </ div >
}
Channel Naming
Best Practices
// Good - descriptive and specific
supabase . channel ( 'chat-room-123' )
supabase . channel ( 'user-notifications-456' )
supabase . channel ( 'game-lobby-xyz' )
// Avoid - too generic
supabase . channel ( 'channel1' )
supabase . channel ( 'test' )
Scoped Channels
Organize channels by feature and ID:
// Chat rooms
const chatChannel = supabase . channel ( `chat: ${ roomId } ` )
// User-specific notifications
const notifChannel = supabase . channel ( `notifications: ${ userId } ` )
// Game sessions
const gameChannel = supabase . channel ( `game: ${ sessionId } ` )
Multiple Listeners
Add multiple event listeners to a single channel:
const channel = supabase
. channel ( 'multi-feature' )
// Listen to database changes
. on (
'postgres_changes' ,
{ event: '*' , schema: 'public' , table: 'messages' },
( payload ) => console . log ( 'Message:' , payload )
)
// Listen to broadcasts
. on (
'broadcast' ,
{ event: 'typing' },
( payload ) => console . log ( 'Typing:' , payload )
)
// Listen to presence
. on (
'presence' ,
{ event: 'sync' },
() => console . log ( 'Presence synced' )
)
. subscribe ()
Private Channels
Implement private channels using RLS:
-- Only allow users to join their own channels
CREATE POLICY "Users can only subscribe to own channel"
ON realtime . messages
FOR SELECT
USING (
channel_name = 'user:' || auth . uid ():: text
);
// Client code
const userId = session . user . id
const channel = supabase . channel ( `user: ${ userId } ` )
Channel Events
System Events
Listen to channel lifecycle events:
const channel = supabase . channel ( 'room-1' )
channel . on ( 'system' , {}, ( payload ) => {
console . log ( 'System event:' , payload )
})
Broadcasting to Channels
Send Messages
Broadcast messages to all subscribers:
const channel = supabase . channel ( 'room-1' )
await channel . send ({
type: 'broadcast' ,
event: 'message' ,
payload: { text: 'Hello everyone!' }
})
Broadcast with Acknowledgment
const channel = supabase . channel ( 'room-1' , {
config: { broadcast: { ack: true } }
})
const response = await channel . send ({
type: 'broadcast' ,
event: 'message' ,
payload: { text: 'Hello!' }
})
if ( response === 'ok' ) {
console . log ( 'Message delivered' )
}
Throttle Updates
Limit the rate of updates:
import { throttle } from 'lodash'
const sendPosition = throttle (( x , y ) => {
channel . send ({
type: 'broadcast' ,
event: 'cursor-pos' ,
payload: { x , y }
})
}, 100 ) // Maximum once per 100ms
Debounce Updates
Delay updates until activity stops:
import { debounce } from 'lodash'
const sendTyping = debounce (() => {
channel . send ({
type: 'broadcast' ,
event: 'typing' ,
payload: { user: 'user-1' }
})
}, 500 ) // Wait 500ms after last keystroke
Error Handling
Handle Connection Errors
const channel = supabase
. channel ( 'room-1' )
. subscribe (( status , err ) => {
if ( status === 'CHANNEL_ERROR' ) {
console . error ( 'Failed to connect:' , err )
// Implement retry logic
setTimeout (() => {
channel . subscribe ()
}, 5000 )
}
})
Handle Timeout
let timeoutId
const channel = supabase
. channel ( 'room-1' )
. subscribe (( status ) => {
if ( status === 'SUBSCRIBED' ) {
clearTimeout ( timeoutId )
} else if ( status === 'TIMED_OUT' ) {
console . error ( 'Connection timed out, retrying...' )
timeoutId = setTimeout (() => {
channel . subscribe ()
}, 3000 )
}
})
Channel Patterns
Chat Room
function createChatRoom ( roomId ) {
const channel = supabase
. channel ( `chat: ${ roomId } ` )
. on (
'postgres_changes' ,
{ event: 'INSERT' , schema: 'public' , table: 'messages' , filter: `room_id=eq. ${ roomId } ` },
( payload ) => {
displayMessage ( payload . new )
}
)
. on (
'broadcast' ,
{ event: 'typing' },
( payload ) => {
showTypingIndicator ( payload . user )
}
)
. on (
'presence' ,
{ event: 'sync' },
() => {
updateOnlineUsers ( channel . presenceState ())
}
)
. subscribe ()
return channel
}
Collaborative Editing
function createCollaborativeEditor ( documentId ) {
const channel = supabase
. channel ( `doc: ${ documentId } ` )
. on (
'broadcast' ,
{ event: 'cursor' },
( payload ) => {
updateRemoteCursor ( payload )
}
)
. on (
'broadcast' ,
{ event: 'change' },
( payload ) => {
applyRemoteChange ( payload )
}
)
. on (
'presence' ,
{ event: 'sync' },
() => {
updateCollaborators ( channel . presenceState ())
}
)
. subscribe ( async ( status ) => {
if ( status === 'SUBSCRIBED' ) {
await channel . track ({
user_id: currentUser . id ,
name: currentUser . name
})
}
})
return channel
}
Live Dashboard
function createLiveDashboard () {
const channel = supabase
. channel ( 'analytics-dashboard' )
. on (
'postgres_changes' ,
{ event: '*' , schema: 'public' , table: 'analytics_events' },
( payload ) => {
updateDashboard ( payload )
}
)
. subscribe ()
return channel
}
Best Practices
Use descriptive channel names
Name channels based on their purpose and scope: supabase . channel ( `chat-room- ${ roomId } ` )
supabase . channel ( `user-notifications- ${ userId } ` )
Always unsubscribe when components unmount: useEffect (() => {
const channel = supabase . channel ( 'my-channel' ). subscribe ()
return () => supabase . removeChannel ( channel )
}, [])
Implement reconnection logic
Handle disconnections gracefully with retry logic.
Throttle high-frequency updates
Use throttling or debouncing for cursor positions, typing indicators, etc.
Use RLS for private channels
Implement Row Level Security to control channel access.
Next Steps
Subscriptions Learn about subscription patterns
Client Libraries Use Realtime with client SDKs