Supabase Realtime Integration for Data Change Subscription
Supabase Realtime allows subscribing to changes in PostgreSQL tables (INSERT, UPDATE, DELETE) and receiving them in browser in real-time via WebSocket. Built on top of PostgreSQL Logical Replication.
Installation
npm install @supabase/supabase-js
Subscribe to Table
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
);
// Subscribe to all changes in messages table
const channel = supabase
.channel('public:messages')
.on(
'postgres_changes',
{
event: '*', // INSERT, UPDATE, DELETE or *
schema: 'public',
table: 'messages',
filter: `room_id=eq.${roomId}` // filter by value
},
(payload) => {
if (payload.eventType === 'INSERT') {
setMessages(prev => [...prev, payload.new]);
}
if (payload.eventType === 'UPDATE') {
setMessages(prev =>
prev.map(m => m.id === payload.new.id ? payload.new : m)
);
}
if (payload.eventType === 'DELETE') {
setMessages(prev => prev.filter(m => m.id !== payload.old.id));
}
}
)
.subscribe((status) => {
console.log('Subscription status:', status);
});
// Unsubscribe
return () => { supabase.removeChannel(channel); };
React Hook
function useRealtimeTable<T>(
table: string,
filter?: { column: string; value: string }
) {
const [data, setData] = useState<T[]>([]);
const supabase = useSupabaseClient();
useEffect(() => {
// Initial load
let query = supabase.from(table).select('*');
if (filter) query = query.eq(filter.column, filter.value);
query.then(({ data }) => setData(data ?? []));
// Subscribe to changes
const channel = supabase.channel(`${table}:${filter?.value ?? 'all'}`)
.on('postgres_changes', {
event: '*',
schema: 'public',
table,
filter: filter ? `${filter.column}=eq.${filter.value}` : undefined
}, (payload) => {
setData(prev => {
if (payload.eventType === 'INSERT') return [...prev, payload.new as T];
if (payload.eventType === 'UPDATE')
return prev.map(item => (item as any).id === (payload.new as any).id
? payload.new as T : item);
if (payload.eventType === 'DELETE')
return prev.filter(item => (item as any).id !== (payload.old as any).id);
return prev;
});
})
.subscribe();
return () => { supabase.removeChannel(channel); };
}, [table, filter?.column, filter?.value]);
return data;
}
// Usage
const messages = useRealtimeTable<Message>('messages', {
column: 'room_id',
value: roomId
});
Broadcast — Custom Events
Broadcast requires no DB changes:
// Send event to all channel subscribers
await supabase.channel('cursor-positions').send({
type: 'broadcast',
event: 'cursor-moved',
payload: { x: mouseX, y: mouseY, userId: user.id }
});
// Receive
supabase.channel('cursor-positions')
.on('broadcast', { event: 'cursor-moved' }, ({ payload }) => {
updateCursorPosition(payload.userId, payload.x, payload.y);
})
.subscribe();
Row Level Security
Realtime respects PostgreSQL RLS — client sees only rows with SELECT access.
-- Example RLS: user sees only their messages
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users see own messages"
ON messages FOR SELECT
USING (auth.uid() = user_id);
Timeline
Basic subscription + React Hook + RLS: 1–2 days.







