Realtime Collaboration
Provider: Supabase Realtime
Protocol: WebSocket (via Supabase Channels)
How It Works
All project state changes are broadcast via Supabase Realtime. Multiple users editing the same project see changes in real time.
mermaid
graph LR
A[Client A edits] --> B[PUT /api/projects/:id]
B --> C[DB update]
C --> D[Postgres trigger]
D --> E[Supabase Realtime]
E --> F[Client B receives]
E --> G[Client C receives]Channel Setup (Client)
svelte
<!-- src/routes/(app)/projects/[id]/+page.svelte -->
<script lang="ts">
import { createClient } from '@supabase/supabase-js'
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
let { data } = $props()
let project = $state(data.project)
const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY)
const channel = supabase
.channel(`project:${data.project.id}`)
.on('postgres_changes', {
event: 'UPDATE',
schema: 'public',
table: 'projects',
filter: `id=eq.${data.project.id}`
}, (payload) => {
project = payload.new
})
.subscribe()
$effect(() => {
return () => supabase.removeChannel(channel)
})
</script>Presence (Who's Online)
Show who else is currently viewing a project:
typescript
const presenceChannel = supabase.channel(`presence:${projectId}`)
presenceChannel
.on('presence', { event: 'sync' }, () => {
const state = presenceChannel.presenceState()
onlineUsers = Object.values(state).flat()
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await presenceChannel.track({
userId: user.id,
name: user.name,
joinedAt: new Date().toISOString()
})
}
})Conflict Resolution
Currently: last-write-wins at the field level. Future: operational transforms for concurrent text editing in Lexical.
Rate Limiting
Supabase Realtime is limited to:
- 200 concurrent connections per project (free tier)
- 10 messages/second per client
For larger agencies, upgrade to Pro or implement message batching.