
Supabase Realtime Not Receiving Events: Complete Fix
A Supabase Realtime subscription can say SUBSCRIBED and still receive nothing. This fix walks through RLS, filter syntax, stale React subscriptions, paused projects, and broadcast versus postgres_changes confusion.

Introduction#
If Supabase Realtime connects but does not receive events, check RLS before you touch the WebSocket code. A SUBSCRIBED status only means the channel joined; it does not prove your Postgres change is visible to that client.
For related Supabase production issues, keep Supabase Realtime gotchas, Supabase RLS silent failures, and Next.js Supabase SSR session management nearby.
Real Reports This Fix Is Based On#
- In supabase-js issue 1730, the report says Realtime "silently fails" when using RLS/JWT.
- In supabase-js issue 1733, the report says: "works without table name" but not with table filtering.
- In Stack Overflow 70599550, the subscription state was "
closed". - In Stack Overflow 78363334, the symptom was: "no events fire."
1. Check RLS First#
Realtime respects the permissions of the subscribing user. If the user cannot select the row after the change, the change may not be delivered to that user.
Start with a plain select using the same client:
const { data, error } = await supabase
.from("messages")
.select("id, room_id, body")
.eq("room_id", roomId)
.limit(1);
console.log({ data, error });If this returns an empty array or an RLS error, your subscription is not the first problem. Fix the select policy.
create policy "room members can read messages"
on public.messages
for select
to authenticated
using (
exists (
select 1
from public.room_members
where room_members.room_id = messages.room_id
and room_members.user_id = auth.uid()
)
);The fix in one line: if a normal .select() cannot see the row, Realtime will not be your reliable escape hatch.
2. Use Exact postgres_changes Filter Syntax#
The current supabase-js v2 API uses channels and postgres_changes. The filter object is not a string like old .from().on() examples.
const channel = supabase
.channel(`room:${roomId}`)
.on(
"postgres_changes",
{
event: "INSERT",
schema: "public",
table: "messages",
filter: `room_id=eq.${roomId}`,
},
(payload) => {
console.log("new message", payload.new);
}
)
.subscribe((status) => {
console.log("realtime status", status);
});Do not write schema:public,table:messages as one string. Use separate keys:
{
event: "*",
schema: "public",
table: "messages"
}If table-specific events fail but schema-wide events work, check spelling, casing, publication settings, and whether you are actually updating the table you subscribed to. The GitHub issue above had exactly that class of confusion: table filtering changed the behavior.
3. Confirm The Table Is In The Realtime Publication#
Realtime does not watch every table by magic. In the Supabase dashboard, enable Realtime for the table, or run SQL that adds it to the supabase_realtime publication.
alter publication supabase_realtime add table public.messages;Then verify:
select *
from pg_publication_tables
where pubname = 'supabase_realtime'
and schemaname = 'public'
and tablename = 'messages';If you need delete payloads with previous values, also check replica identity:
alter table public.messages replica identity full;Use this only where you need it. Full replica identity can increase WAL size.
4. Clean Up React Subscriptions Correctly#
In React and Next.js, stale subscriptions are the second most common problem after RLS. A component mounts, joins a channel, route params change, the old channel stays alive, and now you are debugging duplicate or closed subscriptions.
Use a stable cleanup:
"use client";
import { useEffect } from "react";
export function RoomMessages({ supabase, roomId }) {
useEffect(() => {
const channel = supabase
.channel(`room:${roomId}`)
.on(
"postgres_changes",
{
event: "INSERT",
schema: "public",
table: "messages",
filter: `room_id=eq.${roomId}`,
},
(payload) => {
console.log(payload.new);
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [supabase, roomId]);
return null;
}The dependency array matters. If roomId changes, you want the old channel removed and the new one created. If your Supabase client is recreated on every render, memoize it higher in the tree.
5. Reconnect After Project Pause Or Mobile Background#
Free projects can pause. Mobile apps can background. Browsers can sleep tabs. A channel that was healthy yesterday may not resume with the auth state you expect.
Add visible status logging:
const channel = supabase.channel("debug-room");
channel.subscribe((status, error) => {
console.log("realtime", { status, error });
});For custom JWT or restored sessions, set the Realtime auth token after auth changes:
supabase.auth.onAuthStateChange((_event, session) => {
if (session?.access_token) {
supabase.realtime.setAuth(session.access_token);
}
});This pattern comes straight from real reports where direct queries worked but Realtime did not receive RLS-protected changes after a stored session was loaded.
6. Do Not Confuse Broadcast With Postgres Changes#
Supabase has multiple Realtime features:
postgres_changeslistens to database changes.broadcastsends app-level messages.presencetracks online state.
This listens to database inserts:
supabase.channel("db")
.on("postgres_changes", {
event: "INSERT",
schema: "public",
table: "messages",
}, console.log)
.subscribe();This sends and receives an app event:
const channel = supabase.channel("room");
channel
.on("broadcast", { event: "typing" }, (payload) => {
console.log(payload);
})
.subscribe();
channel.send({
type: "broadcast",
event: "typing",
payload: { userId },
});If you call send() and expect a database row to appear, you are using broadcast. If you insert a row and expect broadcast to fire, you are listening to the wrong event type.
7. Create One Manual Database Change#
When the app has optimistic UI, server actions, retries, and background jobs, it is easy to test the wrong thing. Make one manual database change after the channel subscribes.
Open the SQL editor and run:
insert into public.messages (room_id, user_id, body)
values (
'00000000-0000-0000-0000-000000000001',
'00000000-0000-0000-0000-000000000002',
'realtime test'
);Then watch the browser console. If the manual insert arrives, your Realtime setup works and your app mutation path is suspect. If the manual insert does not arrive, keep debugging publication, RLS, filters, auth token, and cleanup.
For updates, change exactly one row:
update public.messages
set body = body || ' updated'
where id = '00000000-0000-0000-0000-000000000003';This removes a whole category of false leads, especially when a server action updates a different table than the one the client is watching.
8. Log The Payload Shape Before Updating State#
Another quiet failure: the event arrives, but the React state update ignores it because the handler expects the wrong payload.
.on("postgres_changes", config, (payload) => {
console.log("raw realtime payload", payload);
if (payload.eventType === "INSERT") {
setMessages((current) => [...current, payload.new]);
}
if (payload.eventType === "DELETE") {
setMessages((current) =>
current.filter((message) => message.id !== payload.old.id)
);
}
});For DELETE, you may need replica identity to get the old row fields you expect. For UPDATE, compare payload.old and payload.new before assuming the event did not fire.
- If Realtime is not enabled for the table, no client-side code can receive table changes.
- If RLS blocks
selectfor the changed row, the subscribing user may receive nothing. - If the Supabase project is paused or sleeping, reconnect after it wakes up.
Summary#
- Test a normal
.select()with the same client before debugging the channel. - Use separate
event,schema,table, andfilterkeys forpostgres_changes. - Verify the table is in
supabase_realtime. - Remove channels in
useEffectcleanup and include route params in dependencies. - Use
postgres_changesfor database rows,broadcastfor app messages, andpresencefor online state. - Test with one manual insert or update before blaming your UI state code.
One email a month — no fluff
RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.
Continue Reading
Supabase Realtime Gotchas: 7 Issues and How to Fix Them
Avoid common Supabase Realtime pitfalls that cause memory leaks, missed updates, and performance issues. Learn real-world solutions from production applications.
Supabase RLS Not Working: Debug and Fix Policies Step by Step
Master RLS debugging techniques. Learn how to identify, diagnose, and fix Row Level Security policy issues that block data access in production.
Why Your Supabase RLS Policies Are Silently Failing (And How to Debug Them)
RLS failures don't throw errors — they return empty results. Here is exactly how to find and fix the most common Row Level Security bugs in Supabase before they reach production.
Browse by Topic
Find stories that matter to you.
