Realtime with Supabase: WebSockets and Subscriptions
Implement realtime features with Supabase. Broadcast, Presence, Postgres Changes, and a practical chat example.
Written by Francisco Zapata
Realtime features transform static applications into dynamic, interactive experiences. Supabase offers a powerful Realtime system built on Phoenix Channels (Elixir) that provides three primitives: Postgres Changes, Broadcast, and Presence.
Realtime System Architecture
Supabase Realtime is built on Elixir and Phoenix, technologies known for handling millions of concurrent connections. The system operates through WebSockets:
Client ←→ WebSocket ←→ Supabase Realtime Server ←→ PostgreSQL
↕
Other Clients
The Three Primitives
1. Postgres Changes: Listen to INSERT, UPDATE, DELETE on tables
2. Broadcast: Send ephemeral messages between connected clients
3. Presence: Track online user state
Postgres Changes
The most used feature: listening to database changes in real time.
Requirements
First, enable replication for the tables you need:
ALTER PUBLICATION supabase_realtime ADD TABLE posts;
ALTER PUBLICATION supabase_realtime ADD TABLE comments;
Listen to All Changes
const channel = supabase.channel('db-changes')
channel
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'posts' },
(payload) => {
console.log('Change type:', payload.eventType)
console.log('New data:', payload.new)
console.log('Old data:', payload.old)
}
)
.subscribe()
Filter by Event Type
const channel = supabase.channel('new-posts')
.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'posts' },
(payload) => {
console.log('New post:', payload.new.title)
addPostToList(payload.new)
}
)
.subscribe()
Filter by Column
const channel = supabase.channel('my-posts')
.on('postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts',
filter: 'author_id=eq.d0e8c77e-4f2a-4b5c'
},
(payload) => {
console.log('Change in my posts:', payload)
}
)
.subscribe()
Broadcast
Broadcast allows sending messages between clients without persisting them to the database. Ideal for cursors, typing indicators, game positions, etc.
Send and Receive Messages
const channel = supabase.channel('room-1')
channel.on('broadcast', { event: 'message' }, (payload) => {
console.log('Message received:', payload.payload)
})
channel.subscribe((status) => {
if (status === 'SUBSCRIBED') {
channel.send({
type: 'broadcast',
event: 'message',
payload: {
user: 'Francisco',
text: 'Hello everyone!'
}
})
}
})
Typing Indicator
const typingChannel = supabase.channel('chat-typing')
typingChannel.on('broadcast', { event: 'typing' }, ({ payload }) => {
showTypingIndicator(payload.user)
})
typingChannel.subscribe()
let typingTimeout
function onInputChange() {
clearTimeout(typingTimeout)
typingChannel.send({
type: 'broadcast',
event: 'typing',
payload: { user: currentUser.name }
})
typingTimeout = setTimeout(() => {
typingChannel.send({
type: 'broadcast',
event: 'stop-typing',
payload: { user: currentUser.name }
})
}, 2000)
}
Presence
Presence tracks who is connected to a channel in real time. It automatically handles connections and disconnections.
Track Online Users
const presenceChannel = supabase.channel('online-users')
presenceChannel.on('presence', { event: 'sync' }, () => {
const state = presenceChannel.presenceState()
const onlineUsers = Object.values(state).flat()
updateOnlineUsersList(onlineUsers)
})
presenceChannel.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('Joined:', newPresences)
})
presenceChannel.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('Left:', leftPresences)
})
presenceChannel.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await presenceChannel.track({
user_id: currentUser.id,
username: currentUser.name,
avatar: currentUser.avatar,
online_at: new Date().toISOString()
})
}
})
Complete Example: Realtime Chat
Let us combine all three primitives to create a functional chat:
class RealtimeChat {
constructor(roomId, user) {
this.roomId = roomId
this.user = user
this.channel = supabase.channel(`chat:${roomId}`)
this.onMessage = null
this.onPresenceChange = null
this.onTyping = null
}
connect() {
this.channel.on('postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `room_id=eq.${this.roomId}`
},
(payload) => {
if (this.onMessage) this.onMessage(payload.new)
}
)
this.channel.on('broadcast', { event: 'typing' }, ({ payload }) => {
if (this.onTyping && payload.userId !== this.user.id) {
this.onTyping(payload)
}
})
this.channel.on('presence', { event: 'sync' }, () => {
const state = this.channel.presenceState()
if (this.onPresenceChange) {
this.onPresenceChange(Object.values(state).flat())
}
})
this.channel.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await this.channel.track({
userId: this.user.id,
username: this.user.name
})
}
})
}
async sendMessage(text) {
await supabase.from('messages').insert({
room_id: this.roomId,
user_id: this.user.id,
text: text
})
}
sendTyping() {
this.channel.send({
type: 'broadcast',
event: 'typing',
payload: { userId: this.user.id, username: this.user.name }
})
}
disconnect() {
this.channel.unsubscribe()
}
}
Cleanup and Disconnection
Always clean up subscriptions when you no longer need them:
await supabase.removeChannel(channel)
await supabase.removeAllChannels()
In reactive frameworks:
// Vue 3
onUnmounted(() => {
supabase.removeChannel(channel)
})
// React
useEffect(() => {
const channel = supabase.channel('...')
return () => supabase.removeChannel(channel)
}, [])
Performance Considerations
1. Filter on the server: Use filters in postgres_changes to receive only the data you need
2. Limit channels: Do not create unnecessary channels; group related events
3. Throttle broadcasts: For cursors or positions, send updates every 50-100ms
4. Always unsubscribe: Open WebSocket connections consume server resources
Conclusion
Supabase Realtime transforms your applications with three complementary tools. Postgres Changes for data synchronization, Broadcast for ephemeral communication, and Presence for user presence. With these primitives you can build everything from simple notifications to complex collaborative applications.
Comments (0)
Leave a comment
Be the first to comment