Overview
Broadcast allows you to send low-latency messages between connected clients without storing them in the database. It’s perfect for real-time chat, multiplayer games, cursor positions, and any use case where you need fast, ephemeral communication.
Broadcast messages are not persisted. They only exist in memory and are delivered to currently connected clients.
Key Features
Low Latency : Broadcast has minimal overhead compared to database writes
Ephemeral : Messages aren’t stored, reducing database load
Flexible : Send any JSON-serializable data
Scalable : Handles high message volumes efficiently
Basic Usage
Send Messages
Send a broadcast message to all clients subscribed to the channel:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient (
'https://your-project.supabase.co' ,
'your-anon-key'
)
const channel = supabase . channel ( 'room-1' )
// Send a message
channel . send ({
type: 'broadcast' ,
event: 'message' ,
payload: { text: 'Hello world!' , user: 'alice' }
})
channel . subscribe ()
Receive Messages
Listen for broadcast messages:
const channel = supabase . channel ( 'room-1' )
channel
. on ( 'broadcast' , { event: 'message' }, ( payload ) => {
console . log ( 'Received:' , payload )
})
. subscribe ()
Complete Chat Example
Here’s a real-world chat implementation based on the source examples:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient ( URL , KEY )
// Setup channel with self-broadcast enabled
const chatChannel = supabase . channel ( 'chat-room' , {
config: {
broadcast: { self: true } // Receive your own messages
}
})
// Listen for messages
chatChannel
. on ( 'broadcast' , { event: 'message' }, ({ payload }) => {
const { message , user_id , username } = payload
displayMessage ( message , user_id , username )
})
. subscribe (( status ) => {
if ( status === 'SUBSCRIBED' ) {
console . log ( 'Connected to chat' )
}
})
// Send a message
function sendMessage ( text : string ) {
chatChannel . send ({
type: 'broadcast' ,
event: 'message' ,
payload: {
message: text ,
user_id: currentUser . id ,
username: currentUser . name
}
})
}
Advanced Patterns
Multiple Event Types
Handle different message types in the same channel:
const gameChannel = supabase . channel ( 'game-1' )
// Listen to different events
gameChannel
. on ( 'broadcast' , { event: 'player-move' }, ({ payload }) => {
updatePlayerPosition ( payload . playerId , payload . position )
})
. on ( 'broadcast' , { event: 'chat-message' }, ({ payload }) => {
displayChatMessage ( payload . message )
})
. on ( 'broadcast' , { event: 'game-over' }, ({ payload }) => {
showGameOver ( payload . winner )
})
. subscribe ()
// Send different event types
gameChannel . send ({
type: 'broadcast' ,
event: 'player-move' ,
payload: { playerId: 'p1' , position: { x: 100 , y: 200 } }
})
gameChannel . send ({
type: 'broadcast' ,
event: 'chat-message' ,
payload: { message: 'Good game!' }
})
Self-Broadcast Configuration
By default, you don’t receive your own messages. Enable self-broadcast to receive them:
// Don't receive own messages (default)
const channel1 = supabase . channel ( 'room-1' )
// Receive own messages
const channel2 = supabase . channel ( 'room-2' , {
config: {
broadcast: { self: true }
}
})
Collaborative Cursor Example
Track cursor positions in a collaborative editor:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient ( URL , KEY )
const cursorsChannel = supabase . channel ( 'document-cursors' )
// Send cursor position updates
function onMouseMove ( event : MouseEvent ) {
cursorsChannel . send ({
type: 'broadcast' ,
event: 'cursor-move' ,
payload: {
userId: currentUser . id ,
x: event . clientX ,
y: event . clientY ,
timestamp: Date . now ()
}
})
}
// Throttle updates to avoid flooding
const throttledMouseMove = throttle ( onMouseMove , 50 ) // Max 20 updates/second
document . addEventListener ( 'mousemove' , throttledMouseMove )
// Receive other users' cursor positions
cursorsChannel
. on ( 'broadcast' , { event: 'cursor-move' }, ({ payload }) => {
if ( payload . userId !== currentUser . id ) {
updateCursor ( payload . userId , payload . x , payload . y )
}
})
. subscribe ()
Real-World Example: Authorization Chat
This example is based on the nextjs-authorization-demo in the source repository:
'use client'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import { RealtimeChannel } from '@supabase/supabase-js'
import { useState , useEffect } from 'react'
export default function Chat () {
const [ channel , setChannel ] = useState < RealtimeChannel | null >( null )
const [ selectedRoom , setSelectedRoom ] = useState < string | undefined >()
const supabase = createClientComponentClient ()
useEffect (() => {
if ( selectedRoom ) {
channel ?. unsubscribe ()
// Create private channel with broadcast enabled
const newChannel = supabase . channel ( selectedRoom , {
config: {
broadcast: { self: true },
private: true // Requires authorization
}
})
newChannel
. on ( 'broadcast' , { event: 'message' }, ({ payload }) => {
const { message , user_id } = payload
displayMessage ( message , user_id === currentUserId )
})
. subscribe (( status , err ) => {
if ( status === 'SUBSCRIBED' ) {
setChannel ( newChannel )
}
if ( status === 'CHANNEL_ERROR' ) {
console . error ( 'Failed to subscribe:' , err )
}
})
}
}, [ selectedRoom ])
function sendMessage ( text : string ) {
channel ?. send ({
type: 'broadcast' ,
event: 'message' ,
payload: {
message: text ,
user_id: currentUserId
}
})
}
return (
< div >
< div id = "chat-messages" />
< form onSubmit = {(e) => {
e . preventDefault ()
const input = e . target . elements [ 0 ] as HTMLInputElement
sendMessage ( input . value )
input . value = ''
}} >
< input placeholder = "Type a message..." />
< button type = "submit" > Send </ button >
</ form >
</ div >
)
}
Private Channels
Secure your broadcast channels with authorization:
const privateChannel = supabase . channel ( 'private-room' , {
config: {
private: true // Requires authentication and RLS checks
}
})
privateChannel . subscribe ( async ( status ) => {
if ( status === 'SUBSCRIBED' ) {
console . log ( 'Successfully joined private channel' )
}
if ( status === 'CHANNEL_ERROR' ) {
console . log ( 'Not authorized to join this channel' )
}
})
Private channels require users to be authenticated and pass RLS checks defined in your database policies.
Throttle High-Frequency Updates
Avoid overwhelming clients with too many messages:
import { throttle } from 'lodash'
// Limit to 20 updates per second
const broadcastUpdate = throttle (( data ) => {
channel . send ({
type: 'broadcast' ,
event: 'update' ,
payload: data
})
}, 50 )
// Use throttled function
broadcastUpdate ({ x: mouseX , y: mouseY })
Combine multiple updates into one message:
// Instead of sending 3 separate messages:
channel . send ({ type: 'broadcast' , event: 'name' , payload: { name: 'Alice' } })
channel . send ({ type: 'broadcast' , event: 'status' , payload: { status: 'online' } })
channel . send ({ type: 'broadcast' , event: 'avatar' , payload: { avatar: 'url' } })
// Send one message with all data:
channel . send ({
type: 'broadcast' ,
event: 'user-update' ,
payload: {
name: 'Alice' ,
status: 'online' ,
avatar: 'url'
}
})
Message Size Limits
Keep broadcast messages small (< 100KB). Large messages increase latency and network usage.
// Good: Small, focused messages
channel . send ({
type: 'broadcast' ,
event: 'typing' ,
payload: { userId: '123' , isTyping: true }
})
// Avoid: Large payloads
channel . send ({
type: 'broadcast' ,
event: 'data' ,
payload: { hugeArray: [ ... 10000 items ] } // Too large!
})
Combining with Other Features
Broadcast + Presence
Use broadcast for messages and presence for online status:
const channel = supabase . channel ( 'multiplayer-game' , {
config: {
presence: { key: playerId },
broadcast: { self: true }
}
})
// Track presence
channel . track ({ username: 'alice' , level: 5 })
// Send game events
channel . send ({
type: 'broadcast' ,
event: 'attack' ,
payload: { target: 'enemy-1' , damage: 50 }
})
// Listen for everything
channel
. on ( 'presence' , { event: 'join' }, ({ newPresences }) => {
console . log ( 'Players joined:' , newPresences )
})
. on ( 'broadcast' , { event: 'attack' }, ({ payload }) => {
processAttack ( payload )
})
. subscribe ()
Broadcast + Postgres Changes
Persist important messages, use broadcast for ephemeral ones:
const channel = supabase . channel ( 'chat' )
// Listen to persisted messages from database
channel . on (
'postgres_changes' ,
{ event: 'INSERT' , schema: 'public' , table: 'messages' },
({ new : message }) => {
displayPersistedMessage ( message )
}
)
// Listen to ephemeral typing indicators
channel . on ( 'broadcast' , { event: 'typing' }, ({ payload }) => {
showTypingIndicator ( payload . userId )
})
channel . subscribe ()
// Send persisted message
await supabase . from ( 'messages' ). insert ({ text: 'Important message' })
// Send ephemeral typing indicator
channel . send ({
type: 'broadcast' ,
event: 'typing' ,
payload: { userId: currentUser . id }
})
Error Handling
const channel = supabase . channel ( 'chat' )
channel . subscribe (( status , err ) => {
switch ( status ) {
case 'SUBSCRIBED' :
console . log ( 'Connected successfully' )
break
case 'CHANNEL_ERROR' :
console . error ( 'Failed to subscribe:' , err )
// Retry logic here
break
case 'TIMED_OUT' :
console . error ( 'Connection timed out' )
break
case 'CLOSED' :
console . log ( 'Channel closed' )
break
}
})
Next Steps
Presence Track online users and synchronize state
Postgres Changes Listen to database changes in real-time