7.6 KiB
7.6 KiB
| name | description | parent-skill | version | updated |
|---|---|---|---|---|
| realtime-presence | Real-time subscriptions and presence tracking for collaborative features | moai-platform-supabase | 1.0.0 | 2026-01-06 |
Real-time and Presence Module
Overview
Supabase provides real-time capabilities through Postgres Changes (database change notifications) and Presence (user state tracking) for building collaborative applications.
Postgres Changes Subscription
Basic Setup
Subscribe to all changes on a table:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
const channel = supabase.channel('db-changes')
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'messages' },
(payload) => console.log('Change:', payload)
)
.subscribe()
Event Types
Available events:
INSERT- New row addedUPDATE- Row modifiedDELETE- Row removed*- All events
Filtered Subscriptions
Filter changes by specific conditions:
supabase.channel('project-updates')
.on('postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'projects',
filter: `id=eq.${projectId}`
},
(payload) => handleProjectUpdate(payload.new)
)
.subscribe()
Multiple Tables
Subscribe to multiple tables on one channel:
const channel = supabase.channel('app-changes')
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'tasks' },
handleTaskChange
)
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'comments' },
handleCommentChange
)
.subscribe()
Presence Tracking
Presence State Interface
interface PresenceState {
user_id: string
online_at: string
typing?: boolean
cursor?: { x: number; y: number }
}
Channel Setup with Presence
const channel = supabase.channel('room:collaborative-doc', {
config: { presence: { key: userId } }
})
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState<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: userId,
online_at: new Date().toISOString()
})
}
})
Update Presence State
Update user presence in real-time:
// Track typing status
await channel.track({ typing: true })
// Track cursor position
await channel.track({ cursor: { x: 100, y: 200 } })
// Clear typing after timeout
setTimeout(async () => {
await channel.track({ typing: false })
}, 1000)
Collaborative Features
Collaborative Cursors
interface CursorState {
user_id: string
user_name: string
cursor: { x: number; y: number }
color: string
}
function setupCollaborativeCursors(documentId: string, userId: string, userName: string) {
const channel = supabase.channel(`cursors:${documentId}`, {
config: { presence: { key: userId } }
})
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7']
const userColor = colors[Math.abs(userId.hashCode()) % colors.length]
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState<CursorState>()
renderCursors(Object.values(state).flat())
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
user_id: userId,
user_name: userName,
cursor: { x: 0, y: 0 },
color: userColor
})
}
})
// Track mouse movement
document.addEventListener('mousemove', async (e) => {
await channel.track({
user_id: userId,
user_name: userName,
cursor: { x: e.clientX, y: e.clientY },
color: userColor
})
})
return channel
}
Live Editing Indicators
interface EditingState {
user_id: string
user_name: string
editing_field: string | null
}
function setupFieldLocking(formId: string) {
const channel = supabase.channel(`form:${formId}`, {
config: { presence: { key: currentUserId } }
})
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState<EditingState>()
updateFieldLocks(Object.values(state).flat())
})
.subscribe()
return {
startEditing: async (fieldName: string) => {
await channel.track({
user_id: currentUserId,
user_name: currentUserName,
editing_field: fieldName
})
},
stopEditing: async () => {
await channel.track({
user_id: currentUserId,
user_name: currentUserName,
editing_field: null
})
}
}
}
Broadcast Messages
Send arbitrary messages to channel subscribers:
const channel = supabase.channel('room:chat')
// Subscribe to broadcasts
channel
.on('broadcast', { event: 'message' }, ({ payload }) => {
console.log('Received:', payload)
})
.subscribe()
// Send broadcast
await channel.send({
type: 'broadcast',
event: 'message',
payload: { text: 'Hello everyone!', sender: userId }
})
Subscription Management
Unsubscribe
// Unsubscribe from specific channel
await supabase.removeChannel(channel)
// Unsubscribe from all channels
await supabase.removeAllChannels()
Subscription Status
channel.subscribe((status) => {
switch (status) {
case 'SUBSCRIBED':
console.log('Connected to channel')
break
case 'CLOSED':
console.log('Channel closed')
break
case 'CHANNEL_ERROR':
console.log('Channel error')
break
case 'TIMED_OUT':
console.log('Connection timed out')
break
}
})
React Integration
Custom Hook for Presence
import { useEffect, useState } from 'react'
import { supabase } from './supabase/client'
export function usePresence<T>(channelName: string, userId: string, initialState: T) {
const [presences, setPresences] = useState<Record<string, T[]>>({})
useEffect(() => {
const channel = supabase.channel(channelName, {
config: { presence: { key: userId } }
})
channel
.on('presence', { event: 'sync' }, () => {
setPresences(channel.presenceState<T>())
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track(initialState)
}
})
return () => {
supabase.removeChannel(channel)
}
}, [channelName, userId])
const updatePresence = async (state: Partial<T>) => {
const channel = supabase.getChannels().find(c => c.topic === channelName)
if (channel) {
await channel.track({ ...initialState, ...state } as T)
}
}
return { presences, updatePresence }
}
Usage
function CollaborativeEditor({ documentId, userId }) {
const { presences, updatePresence } = usePresence(
`doc:${documentId}`,
userId,
{ user_id: userId, typing: false, cursor: null }
)
return (
<div>
{Object.values(presences).flat().map(p => (
<Cursor key={p.user_id} position={p.cursor} />
))}
</div>
)
}
Context7 Query Examples
For latest real-time documentation:
Topic: "realtime postgres_changes subscription" Topic: "presence tracking channel" Topic: "broadcast messages supabase"
Related Modules:
- typescript-patterns.md - Client architecture
- auth-integration.md - Authenticated subscriptions