Supabase Realtime is built on PostgreSQL's built-in logical replication. When you enable Realtime on a table, Postgres streams every INSERT, UPDATE, and DELETE as a change event. The Supabase Realtime server picks up these events from the replication slot and pushes them to connected WebSocket clients. The whole pipeline is event-driven — there is no polling, no interval timers, no wasted database queries. Changes propagate from the database to the browser in milliseconds.
This architecture means Realtime is as fast as your network and database allow. It also means Realtime is a server-side feature: you enable it on the database, not the client. The client simply subscribes to an existing stream.
supabase_realtime publication before clients can receive changes. Skipping this step is the most common reason Realtime subscriptions appear to do nothing.
In the Supabase Dashboard, go to Database > Replication and toggle the tables you want to replicate. Or use SQL:
alter publication supabase_realtime add table posts;
To remove a table from Realtime:
alter publication supabase_realtime drop table posts;
You can verify which tables are in the publication with:
SELECT * FROM pg_publication_tables WHERE pubname = 'supabase_realtime';
Once a table is added, every write to it generates a change event that clients can subscribe to.
On the client side, subscribing is a three-step process: create a channel, listen for events, and subscribe.
const channel = supabase
.channel('posts-changes')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'posts' },
(payload) => {
console.log('New post:', payload.new);
}
)
.subscribe();
The .channel() call creates a named channel (choose any unique name). The .on() call registers a listener for a specific combination of event type, schema, and table. The .subscribe() call opens the WebSocket connection.
The payload object contains:
eventType — 'INSERT', 'UPDATE', or 'DELETE'new — the new row (for INSERT and UPDATE)old — the old row (for UPDATE and DELETE)schema and table — the sourceThe event parameter accepts 'INSERT', 'UPDATE', 'DELETE', or '*' for all three. You can subscribe to only the events you care about:
// Listen only for deletes
.on('postgres_changes', { event: 'DELETE', schema: 'public', table: 'posts' }, callback)
// Listen for everything
.on('postgres_changes', { event: '*', schema: 'public', table: 'posts' }, callback)
You can add a filter to receive only matching changes. The filter syntax uses column=value:
const channel = supabase
.channel('my-posts')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'posts', filter: `user_id=eq.${user.id}` },
(payload) => {
console.log('Change to my post:', payload);
}
)
.subscribe();
This listener fires only when a row is inserted, updated, or deleted where user_id matches the current user. Filters are evaluated by the Realtime server, so no unwanted events cross the network.
Beyond database change streaming, Supabase Realtime also supports broadcast (send arbitrary messages to all clients on a channel) and presence (track who is currently connected). These are useful for features like cursor positions in collaborative editing, typing indicators, or "10 users viewing this page" counters. Broadcast and presence do not touch the database at all — they are purely WebSocket-based.
// On page leave or component unmount
channel.unsubscribe();
posts table and increment a counter in the DOM. Do not forget to enable Realtime on the table first.