| name | supabase-realtime |
| description | Use when implementing Supabase Realtime features - broadcast, presence, or database change subscriptions. Includes private channel setup, RLS policies, and migration patterns. |
Supabase Realtime
When to Use
- Live data updates (items appear without refresh)
- Co-editing (multiple users editing the same document)
- Presence (who's online, cursor positions)
- Ephemeral events (typing indicators, cursor moves)
Pattern Selection
| Pattern | Use Case | Persistence |
|---|
| Broadcast | Typing, cursor moves, custom events | None |
| Presence | Online users, cursors, session state | Session only |
| Postgres Changes | Row CRUD events (legacy — prefer broadcast) | Yes (DB) |
Quick Reference
Do
- Use
broadcast for all realtime events (prefer over postgres_changes)
- Use
private: true for channels that need auth
- Call
setAuth() before subscribing to private channels
- Use topic pattern:
scope:id:entity (e.g., tasks:{startupId}:events)
- Use event pattern:
entity_action (e.g., task_created)
- Include cleanup in useEffect return
- Create indexes for RLS policy columns
Don't
- Use
postgres_changes for new apps (single-threaded, doesn't scale)
- Call
realtime.send or realtime.broadcast_changes from client code
- Subscribe without cleanup logic
- Use generic event names like "update" or "change"
Client Setup
const channel = supabase.channel(`tasks:${startupId}:events`, {
config: {
broadcast: { self: true, ack: true },
private: true
}
});
await supabase.realtime.setAuth();
channel
.on('broadcast', { event: 'task_created' }, handleTaskCreated)
.on('broadcast', { event: 'task_updated' }, handleTaskUpdated)
.subscribe();
return () => supabase.removeChannel(channel);
React Hook Pattern
const channelRef = useRef<RealtimeChannel | null>(null);
useEffect(() => {
if (!startupId) return;
if (channelRef.current?.state === 'subscribed') return;
const channel = supabase.channel(`tasks:${startupId}:events`, {
config: { private: true }
});
channelRef.current = channel;
channel
.on('broadcast', { event: 'task_created' }, ({ payload }) => {
})
.subscribe();
supabase.realtime.setAuth();
return () => {
if (channelRef.current) {
supabase.removeChannel(channelRef.current);
channelRef.current = null;
}
};
}, [startupId]);
Presence
const channel = supabase.channel(`room:${roomId}`);
channel
.on("presence", { event: "sync" }, () => {
setUsers(Object.values(channel.presenceState()).flat());
})
.subscribe(async (status) => {
if (status === "SUBSCRIBED") await channel.track({ user_id, name });
});
return () => supabase.removeChannel(channel);
Client-Side Broadcast (Ephemeral)
const channel = supabase.channel(name)
.on("broadcast", { event: "cursor" }, ({ payload }) => { })
.subscribe();
channel.send({ type: "broadcast", event: "cursor", payload });
Legacy: Postgres Changes with React Query
Prefer broadcast for new apps. This pattern is for existing postgres_changes subscriptions.
useEffect(() => {
const channel = supabase
.channel(`${table}-changes`)
.on("postgres_changes", { event: "*", schema: "public", table }, () => {
queryClient.invalidateQueries({ queryKey });
})
.subscribe();
return () => { supabase.removeChannel(channel); };
}, [table]);
Database Trigger (Broadcast)
CREATE OR REPLACE FUNCTION broadcast_task_event()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
topic_name text;
event_name text;
payload jsonb;
BEGIN
event_name := TG_OP || '_' || lower(TG_TABLE_NAME);
topic_name := TG_TABLE_NAME || ':' || NEW.startup_id::text || ':events';
payload := jsonb_build_object(
'id', NEW.id,
'event', event_name,
'timestamp', now()
);
PERFORM realtime.send(payload, event_name, topic_name, TRUE);
RETURN NEW;
END;
$$;
CREATE TRIGGER task_broadcast
AFTER INSERT OR UPDATE ON tasks
FOR EACH ROW EXECUTE FUNCTION broadcast_task_event();
RLS for Private Channels
CREATE POLICY "Users can join startup channels"
ON realtime.messages
FOR SELECT TO authenticated
USING (
EXISTS (
SELECT 1 FROM startups s
INNER JOIN profiles p ON p.org_id = s.org_id
WHERE p.id = (SELECT auth.uid())
AND realtime.topic() LIKE '%:' || s.id::text || ':%'
)
);
CREATE INDEX idx_profiles_org_id ON profiles(org_id);
CREATE INDEX idx_startups_org_id ON startups(org_id);
Migration Checklist
Implementation Checklist
Resources