Overview
Postgres Changes allows you to subscribe to real-time changes in your PostgreSQL database. When a row is inserted, updated, or deleted, Realtime broadcasts the change to all subscribed clients.
Postgres Changes uses PostgreSQL’s replication functionality. Changes are captured from the Write-Ahead Log (WAL) and broadcast to connected clients.
Enable Realtime
Before subscribing to changes, you need to enable Realtime for your tables:
Via Dashboard
Go to the Database page in your Supabase Dashboard
Click on “Replication” in the sidebar
Toggle Realtime for the tables you want to watch
Via SQL
ALTER PUBLICATION supabase_realtime ADD TABLE messages;
Basic Usage
Listen to All Changes
Subscribe to all events (INSERT, UPDATE, DELETE) on a table:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient (
'https://your-project.supabase.co' ,
'your-anon-key'
)
supabase
. channel ( 'schema-db-changes' )
. on (
'postgres_changes' ,
{
event: '*' ,
schema: 'public' ,
table: 'messages'
},
( payload ) => {
console . log ( 'Change received!' , payload )
}
)
. subscribe ()
Listen to Specific Events
Subscribe only to INSERT events:
supabase
. channel ( 'public:messages:insert' )
. on (
'postgres_changes' ,
{
event: 'INSERT' ,
schema: 'public' ,
table: 'messages'
},
( payload ) => {
console . log ( 'New message:' , payload . new )
}
)
. subscribe ()
Available events:
INSERT - New rows inserted
UPDATE - Existing rows updated
DELETE - Rows deleted
* - All events
Filter by Column Values
Listen to changes only when specific column values match:
supabase
. channel ( 'room-messages' )
. on (
'postgres_changes' ,
{
event: 'INSERT' ,
schema: 'public' ,
table: 'messages' ,
filter: 'room_id=eq.123'
},
( payload ) => {
console . log ( 'New message in room 123:' , payload . new )
}
)
. subscribe ()
Supported filter operators:
eq - Equals
neq - Not equals
lt - Less than
lte - Less than or equal
gt - Greater than
gte - Greater than or equal
Payload Structure
The payload object contains information about the change:
{
commit_timestamp : '2024-03-04T10:30:00Z' ,
errors : null ,
eventType : 'INSERT' ,
new : {
id : 1 ,
text : 'Hello world' ,
user_id : 'abc-123' ,
created_at : '2024-03-04T10:30:00Z'
},
old : {}, // Empty for INSERT
schema : 'public' ,
table : 'messages'
}
Event Type Differences
INSERT : new contains the inserted row, old is empty
{
eventType : 'INSERT' ,
new : { id : 1 , text : 'New message' },
old : {}
}
UPDATE : Both new and old contain data
{
eventType : 'UPDATE' ,
new : { id : 1 , text : 'Updated message' },
old : { id : 1 , text : 'Original message' }
}
DELETE : old contains the deleted row, new is empty
{
eventType : 'DELETE' ,
new : {},
old : { id : 1 , text : 'Deleted message' }
}
Advanced Patterns
Multiple Table Subscriptions
Listen to changes across multiple tables using one channel:
const channel = supabase . channel ( 'database-changes' )
channel
. on (
'postgres_changes' ,
{ event: '*' , schema: 'public' , table: 'messages' },
handleMessageChange
)
. on (
'postgres_changes' ,
{ event: '*' , schema: 'public' , table: 'users' },
handleUserChange
)
. subscribe ()
Real-time Dashboard Example
Automatically update a dashboard when metrics change:
import { createClient } from '@supabase/supabase-js'
import { useState , useEffect } from 'react'
function MetricsDashboard () {
const [ metrics , setMetrics ] = useState ([])
const supabase = createClient ( URL , KEY )
useEffect (() => {
// Fetch initial data
supabase
. from ( 'metrics' )
. select ( '*' )
. then (({ data }) => setMetrics ( data ))
// Subscribe to changes
const channel = supabase
. channel ( 'metrics-changes' )
. on (
'postgres_changes' ,
{ event: '*' , schema: 'public' , table: 'metrics' },
( payload ) => {
if ( payload . eventType === 'INSERT' ) {
setMetrics (( prev ) => [ ... prev , payload . new ])
} else if ( payload . eventType === 'UPDATE' ) {
setMetrics (( prev ) =>
prev . map (( m ) => ( m . id === payload . new . id ? payload . new : m ))
)
} else if ( payload . eventType === 'DELETE' ) {
setMetrics (( prev ) => prev . filter (( m ) => m . id !== payload . old . id ))
}
}
)
. subscribe ()
return () => {
channel . unsubscribe ()
}
}, [])
return < div >{ /* Render metrics */ } </ div >
}
Live Query Pattern
Combine initial fetch with real-time updates:
async function setupLiveQuery () {
// 1. Fetch initial data
const { data : initialData } = await supabase
. from ( 'messages' )
. select ( '*' )
. order ( 'created_at' , { ascending: false })
. limit ( 50 )
displayMessages ( initialData )
// 2. Subscribe to new changes
supabase
. channel ( 'new-messages' )
. on (
'postgres_changes' ,
{ event: 'INSERT' , schema: 'public' , table: 'messages' },
( payload ) => {
addMessage ( payload . new )
}
)
. subscribe ()
}
Authorization with RLS
Postgres Changes respects Row Level Security policies. Clients only receive changes for rows they have permission to see.
Enable RLS
ALTER TABLE messages ENABLE ROW LEVEL SECURITY ;
CREATE POLICY "Users can only see their own messages"
ON messages
FOR SELECT
USING ( auth . uid () = user_id);
Now authenticated users only receive real-time updates for their own messages:
// User only receives changes for their messages
const supabase = createClient ( URL , KEY , {
global: {
headers: { Authorization: `Bearer ${ userToken } ` }
}
})
supabase
. channel ( 'my-messages' )
. on (
'postgres_changes' ,
{ event: '*' , schema: 'public' , table: 'messages' },
( payload ) => {
console . log ( 'My message changed:' , payload )
}
)
. subscribe ()
Without RLS policies, all users can see all database changes. Always enable RLS for production applications.
Limit the Number of Subscriptions
Each subscription creates overhead. Combine related subscriptions when possible:
// Better: One channel with filters
const channel = supabase . channel ( 'all-events' )
channel
. on ( 'postgres_changes' , { event: 'INSERT' , schema: 'public' , table: 'messages' }, handler )
. on ( 'postgres_changes' , { event: 'UPDATE' , schema: 'public' , table: 'messages' }, handler )
. subscribe ()
// Avoid: Multiple channels for same table
// supabase.channel('inserts').on(...)
// supabase.channel('updates').on(...)
Use Column Filters
Filter at the database level rather than in your application:
// Good: Filter server-side
supabase
. channel ( 'active-users' )
. on (
'postgres_changes' ,
{
event: 'UPDATE' ,
schema: 'public' ,
table: 'users' ,
filter: 'status=eq.active'
},
handler
)
. subscribe ()
// Avoid: Filter client-side
supabase
. channel ( 'all-users' )
. on (
'postgres_changes' ,
{ event: 'UPDATE' , schema: 'public' , table: 'users' },
( payload ) => {
if ( payload . new . status === 'active' ) {
handler ( payload )
}
}
)
Clean Up Subscriptions
Always unsubscribe when done:
const channel = supabase . channel ( 'temp' )
// ... use channel
// Clean up
await channel . unsubscribe ()
Troubleshooting
No Changes Received
Check if Realtime is enabled for the table in the Dashboard
Verify RLS policies aren’t blocking changes
Confirm subscription status :
channel . subscribe (( status ) => {
console . log ( 'Status:' , status )
if ( status === 'SUBSCRIBED' ) {
console . log ( 'Successfully subscribed' )
}
})
Changes Delayed
Realtime uses PostgreSQL’s logical replication, which has minimal latency (typically < 100ms)
Network conditions affect delivery time
Check your project’s health in the Dashboard
Next Steps
Broadcast Send ephemeral messages between clients
Presence Track online users in real-time