Advanced Realtime Patterns
Supabase Realtime goes beyond database change listeners. With Presence, Broadcast, and channel authorization, you can build collaborative features like online status indicators, shared cursors, live editing, and ephemeral messaging — all without a separate WebSocket server.
🚀 Realtime Features
- Presence: Track who is online, typing indicators, cursor positions
- Broadcast: Send ephemeral messages to channel subscribers
- Postgres Changes: Listen to INSERT, UPDATE, DELETE events
- Authorization: Secure channels with RLS-like policies
Presence: Online Status & Typing Indicators
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
// Track who is online in a room
const channel = supabase.channel('room-1', {
config: { presence: { key: userId } },
})
// Subscribe to presence changes
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
const onlineUsers = Object.keys(state)
console.log('Online users:', onlineUsers)
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('User joined:', key)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('User left:', key)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// Track this user's presence
await channel.track({
user_id: userId,
username: 'John',
online_at: new Date().toISOString(),
})
}
})
// Typing indicator — update presence state
async function setTyping(isTyping: boolean) {
await channel.track({
user_id: userId,
username: 'John',
is_typing: isTyping,
online_at: new Date().toISOString(),
})
}
Broadcast: Ephemeral Messages
// Broadcast sends messages without persisting them
// Perfect for cursor positions, reactions, notifications
const channel = supabase.channel('document-123')
// Listen for cursor movements from other users
channel
.on('broadcast', { event: 'cursor-move' }, (payload) => {
const { userId, x, y } = payload.payload
updateCursorPosition(userId, x, y)
})
.on('broadcast', { event: 'reaction' }, (payload) => {
showReaction(payload.payload.emoji)
})
.subscribe()
// Send cursor position (not persisted to database)
function onMouseMove(e: MouseEvent) {
channel.send({
type: 'broadcast',
event: 'cursor-move',
payload: {
userId,
x: e.clientX,
y: e.clientY,
},
})
}
// Send a reaction
function sendReaction(emoji: string) {
channel.send({
type: 'broadcast',
event: 'reaction',
payload: { emoji, userId },
})
}
Postgres Changes with Filters
// Listen to specific database changes
const channel = supabase
.channel('project-updates')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: 'room_id=eq.room-123', // Only this room's messages
},
(payload) => {
console.log('New message:', payload.new)
addMessageToUI(payload.new)
}
)
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'messages',
filter: 'room_id=eq.room-123',
},
(payload) => {
console.log('Edited message:', payload.new)
updateMessageInUI(payload.new)
}
)
.subscribe()
Building Collaborative Features
// Collaborative document editing with shared cursors
function useCollaborativeEditor(documentId: string) {
const channel = supabase.channel(`doc-${documentId}`, {
config: { presence: { key: userId } },
})
// Track user presence with cursor position and selection
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
// Render collaborator cursors
Object.entries(state).forEach(([key, presences]) => {
if (key !== userId) {
const data = presences[0] as any
renderCollaboratorCursor(key, data.cursor, data.color)
}
})
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
user_id: userId,
username: currentUser.name,
color: getRandomColor(),
cursor: { line: 0, ch: 0 },
})
}
})
// Update cursor position as user types
function onCursorChange(cursor: { line: number; ch: number }) {
channel.track({
user_id: userId,
username: currentUser.name,
color: assignedColor,
cursor,
})
}
return { channel, onCursorChange }
}
⚠️ Performance Tips
Throttle high-frequency events like cursor movements to 50-100ms intervals. Unsubscribe from channels when components unmount. Use Broadcast for ephemeral data (cursors, reactions) and Postgres Changes only for persistent data that needs database writes.
💡 Key Takeaways
- • Presence tracks who is online and their state (typing, cursor position)
- • Broadcast sends ephemeral messages without database writes
- • Use filters on Postgres Changes to reduce unnecessary events
- • Throttle high-frequency events for better performance
- • Always clean up channel subscriptions on component unmount
📚 Learn More
-
Realtime Presence →
Track and synchronize shared state between users.
-
Realtime Broadcast →
Send low-latency ephemeral messages to channel subscribers.