Supabase Realtime Not Working: Why Am I Not Receiving Updates?
"I Built a Chat App, But I Have to Refresh to See Messages"
I was building a chat app using Supabase Realtime.
I followed the docs and subscribed: supabase.channel('room-1').on(...).subscribe().
const channel = supabase
.channel('room1')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages' }, (payload) => {
console.log('New message!', payload);
})
.subscribe();
I sent a message from another window.
The message was inserted into the DB correctly.
But... my console was silent. Nothing happened.
I had to Refresh (F5) to see the new message. Use F5 for Realtime? That's not Realtime!
What Confused Me Initially? (Connected but Silent?)
I checked the Network tab. Websocket connection was successful (101 Switching Protocols).
I checked the Realtime Inspector in the Supabase Dashboard. No messages there either.
"Is the server down? No, it's connected. Is my code wrong?" I checked for typos dozens of times. Nothing was wrong.
The 'Aha!' Moment (The Broadcast Button)
The problem wasn't in the code, but in the DB Settings. Supabase (Postgres) defaults to "Do NOT broadcast changes for this table."
Why? If it broadcasted changes for every single table, the server load would be massive. So you must explicitly say, "You are allowed to broadcast this table."
Analogy: The Broadcasting Station (Supabase) has Cameras (Realtime Server) installed. But the News Anchor's (Table) microphone was OFF (Replication Off). No matter how much the anchor shouted (INSERT), no sound went out to the Broadcast (Subscriber).
The Fix: Enable Replication
You fix this in the Supabase Dashboard, not in code.
- Go to Supabase Dashboard -> Database -> Replication.
- Find the
messagestable. - Toggle the Source switch on. (Enable Replication)
Or do it via SQL:
-- Tell Postgres to publish changes for 'messages' table
alter publication supabase_realtime add table "messages";
As soon as I enabled this, logs started flooding my console. "Wow, I forgot to turn on the mic."
Deep Dive: Postgres WAL & RLS
Supabase Realtime isn't magic. It reads the Postgres WAL (Write Ahead Log). When DB changes happen, a log is written. The Realtime server tails this log and shoots it over Websocket.
1. What if RLS is On?
Here's another trap. RLS (Row Level Security).
If messages table has RLS, and a policy says "Cheolsu cannot see Yeonghi's messages."
The Realtime server respects this. If Yeonghi writes a message, Cheolsu's subscription (Channel) will not receive the event. Supabase filters it out automatically.
So if "Replication is on, but no event?", check your SELECT RLS policy.
If you can't see the data, you won't be notified about it either.
2. Empty Payload? (Replica Identity)
Sometimes you get an UPDATE or DELETE event, but the old data field is empty.
Postgres only logs changed columns by default for performance.
If you need the entire row data when a row is deleted (not just the ID), change the table setting:
ALTER TABLE "messages" REPLICA IDENTITY FULL;
This logs all column data on change. (Slight DB performance cost).
Application: Handling Connection State
In production, you should show "Connected/Disconnected" state in the UI.
const channel = supabase.channel('room1')
.on('postgres_changes', ...)
.subscribe((status) => {
if (status === 'SUBSCRIBED') {
console.log('🟢 Connected');
setIsConnected(true);
}
if (status === 'CHANNEL_ERROR') {
console.log('🔴 Connection Failed');
setIsConnected(false);
}
if (status === 'TIMED_OUT') {
console.log('🟡 Timeout (Retrying...)');
}
});
Especially on mobile (Flutter/React Native), connections drop when the app goes background. Detecting this and showing "Reconnecting..." is essential UX.
7. Deep Dive: "Who Is Online?" (Presence)
Realtime isn't just for DB changes. Presence tracks who is connected. It shares state between clients via WebSocket (Memory-based, No DB access).
const channel = supabase.channel('room-1');
channel
.on('presence', { event: 'sync' }, () => {
const newState = channel.presenceState();
console.log('Online Users:', newState);
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// I am here!
await channel.track({ user_id: 'user_123', status: 'online' });
}
});
Use this for "User is typing..." features.
8. Case Study: Collaborative Cursors (Figma Style)
Building a collaborative whiteboard? You need to show others' mouse cursors.
If you INSERT mouse coordinates into the DB 60 times a second, your DB will explode in 1 second.
The Fix: Broadcast Mode
Supabase Realtime supports Broadcast. It bypasses the DB entirely and sends data client-to-client.
// On mouse move
channel.send({
type: 'broadcast',
event: 'cursor-pos',
payload: { x: 100, y: 200 }
});
It's volatile and fast. Perfect for games or cursors. (Do NOT use Postgres Changes for this!)
9. Refactoring Challenge: Graceful Reconnection
Problem: WiFi drops in the subway and reconnects 5 seconds later. Supabase SDK reconnects automatically, but messages sent during those 5 seconds are lost forever. The user has no idea they missed part of the conversation.
Challenge:
Whenever SUBSCRIBED status returns, Fetch data strictly AFTER the last received ID.
/* Pseudo Code */
let lastReceivedId = 0;
channel.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// 1. Check last known state
if (lastReceivedId > 0) {
// 2. Fetch missing data since disconnect
const missed = await fetchMessages({ id_gt: lastReceivedId });
mergeMessages(missed);
}
}
});
channel.on(..., (payload) => {
lastReceivedId = payload.new.id;
updateUI(payload.new);
});
Without this "Gap Filling" logic, your chat app is fragile.
10. Deep Dive: Postgres Publication Model
Behind Supabase Realtime sits the postgres_output logical decoding plugin.
It converts WAL entries into JSON.
Enabling Replication in the dashboard runs:
CREATE PUBLICATION supabase_realtime FOR TABLE messages;
Architectural Considerations:
- Performance:
REPLICA IDENTITY FULLincreases WAL size significantly. Use it only when you absolutely need theoldrecord data (e.g., needed to delete a file associated with the deleted row). - Toast Columns: Massive text fields might be skipped in WAL to save space. Avoid relying on huge JSONB payload updates in Realtime filters.
11. FAQ: Trigger vs Realtime
Q: Can't I just use a Postgres Trigger to call an API? A:
- Trigger: Runs inside DB transaction. Ensures data integrity. slow.
- Realtime: Reads WAL asynchronously. Zero impact on DB performance. Just for notifications.
Use Realtime for Chat. Use Triggers (calling Edge Functions) for "Welcome Email on Signup".
One-Line Summary
Realtime not working? 99% of the time, you forgot to enable 'Replication'. Turn on the 'Broadcast Mic' for your table in the Dashboard. If that fails, check RLS.