What this usually means
Supabase Realtime relies on PostgreSQL logical replication slots. When a client subscribes, it creates a replication slot. Common failures: the slot is full (wal_level not logical), RLS policies block the change from being broadcast (Realtime respects RLS), the table isn't in the publication, or the filter expression (e.g., `event: 'INSERT'`) is misconfigured. Also, if you're using Supabase's Realtime Server (self-hosted), the API key might be missing the `realtime` scope.
The first ten minutes — establish facts before touching code.
- 1Check WebSocket connection: In browser DevTools → Network → WS, open the realtime URL (wss://<project>.supabase.co/realtime/v1/websocket). Verify status 101 and heartbeat messages every 30s.
- 2Test with Supabase's Realtime Inspector: Go to your project Dashboard → Database → Realtime → Inspector. Subscribe to a table and run an INSERT. If events appear here but not in your app, it's a client-side issue.
- 3Verify table is in the publication: Run `SELECT * FROM pg_publication_tables WHERE pubname = 'supabase_realtime';` in your SQL editor. If the table is missing, add it.
- 4Check RLS permissions: If the table has RLS enabled, the realtime event won't be sent unless the user (or the service_role key) has SELECT permission. Try with `service_role` key to rule out RLS.
- 5Examine replication slots: Run `SELECT slot_name, active, restart_lsn FROM pg_replication_slots;` on your database. If slots are inactive, the subscriber may have disconnected.
- 6Test with a simple filter: Subscribe without any `event` or `schema` filters. If events come through, add filters one by one.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchSupabase Dashboard → Database → Realtime → Inspector (live event viewer)
- searchSupabase Dashboard → SQL Editor → Run: `SELECT * FROM pg_publication_tables WHERE pubname = 'supabase_realtime';`
- searchSupabase Dashboard → SQL Editor → Run: `SELECT slot_name, active, restart_lsn FROM pg_replication_slots;`
- searchBrowser DevTools → Network tab → WebSocket frames for the realtime connection
- searchApplication console logs: enable debugging with `supabase.realtime.setDebug(true)`
- searchSupabase Realtime Server logs (if self-hosted): `/var/log/realtime/server.log`
Practical causes, not theory. These are the things you will actually find.
- warningTable not added to the `supabase_realtime` publication
- warningRow-Level Security (RLS) blocking the event for the authenticated user
- warningReplication slot inactive due to client disconnect or wal_level misconfiguration
- warningFilter mismatch: client subscribes with `event: 'INSERT'` but the actual change is an UPDATE
- warningUsing the wrong API key (anon key without `realtime` scope or service_role key without RLS bypass)
- warningSchema not specified in subscription when using non-public schemas
Concrete fix directions. Pick the one that matches your root cause.
- buildAdd table to publication: `ALTER PUBLICATION supabase_realtime ADD TABLE your_table;`
- buildFor RLS: either disable RLS on the table (not recommended), or ensure the user has SELECT permission. For service_role, RLS is bypassed by default.
- buildIf replication slot is inactive, reconnect the client or restart the Realtime server. For persistent issues, increase max_replication_slots in PostgreSQL config.
- buildDouble-check subscription filters: remove all filters and add them back one by one.
- buildUse the correct API key: for public access, use the anon key with `realtime` scope enabled in Supabase Dashboard → Settings → API.
A fix you cannot prove is a guess. Close the loop.
- verifiedAfter fix, use the Realtime Inspector to confirm events appear.
- verifiedIn your app, add a console log on subscription callback: `channel.on('postgres_changes', { event: '*', schema: 'public', table: 'test' }, payload => console.log('Received:', payload)).subscribe()`
- verifiedRun an INSERT/UPDATE/DELETE from the SQL Editor and watch the WebSocket frames for a 'postgres_changes' message.
- verifiedCheck replication slot becomes active: `SELECT slot_name, active FROM pg_replication_slots;` should show active = t for your slot.
- verifiedStress test: perform 100 rapid inserts and verify all events are received (no gaps).
Things that make this bug worse or harder to find.
- warningDon't assume the client library is at fault before verifying actual WebSocket messages.
- warningDon't forget that Realtime respects RLS. If you use anon key without proper policies, events are silently dropped.
- warningDon't subscribe with `table: 'users'` if you're using the `auth.users` table (it's in `auth` schema, not `public`). Use `schema: 'auth'`.
- warningDon't ignore the 'heartbeat' message: if you see heartbeats but no data, the issue is on the database side, not the client.
- warningDon't set `max_replication_slots` too low; each client consumes one slot. Default is 10, which may be insufficient for many concurrent users.
Realtime Events Silent After RLS Policy Change
Timeline
- 09:15Deploy new RLS policy to users table for GDPR compliance.
- 09:20User reports that profile updates are not reflecting in realtime on dashboard.
- 09:25Check Supabase Realtime Inspector: no events for 'users' table.
- 09:30Run `SELECT * FROM pg_publication_tables` — table is in publication.
- 09:35Check replication slots: one slot is active for the client.
- 09:40Suspect RLS: temporarily disable RLS on users table — events start flowing.
- 09:45Review new RLS policy: it uses `auth.uid() = user_id` but the realtime subscription uses anon key (no uid).
- 09:50Fix: add a policy for anon users to SELECT their own data (or use service_role key).
- 09:55Re-enable RLS, events work for authenticated users.
I pushed a new RLS policy to the users table that required `auth.uid() = user_id` for SELECT. I thought I was tightening security. Within minutes, the product team flagged that the realtime dashboard stopped updating when users changed their profiles.
I went straight to the Realtime Inspector — no events. I checked the publication, replication slots, even restarted the Realtime server. Nothing. Then I remembered: Realtime respects RLS. The anon key we used for the subscription had no user context, so the policy evaluated to false and the event was silently dropped.
The fix was either to use the service_role key (bypasses RLS) or add a policy that allows anon users to SELECT. We went with a policy that allows SELECT if the row's user_id matches the request's claim (for authenticated users) and a separate policy for anon to see only public fields. Tested and verified.
Root cause
New RLS policy on users table blocked SELECT for anonymous key, causing Realtime to suppress the event.
The fix
Added an RLS policy allowing SELECT for authenticated users and used service_role key for the subscription, or added a policy for anon users.
The lesson
Always test Realtime subscriptions after any RLS change. The Realtime Inspector is your first stop.
Supabase Realtime uses PostgreSQL's logical replication. When you subscribe, the Realtime server creates a replication slot that listens for changes on tables in the `supabase_realtime` publication. The slot catches INSERT, UPDATE, DELETE, and TRUNCATE events.
Each event is then filtered by the client's subscription parameters (schema, table, filter). The event is only sent if the user has SELECT permission on the row (RLS check). This is a common source of confusion: events are not broadcast to all subscribers; they are scoped to the user's permissions.
Each WebSocket connection uses one replication slot. If the client disconnects, the slot becomes inactive but remains until the next restart or manual cleanup. Too many idle slots can slow down WAL cleanup and cause disk bloat.
Run `SELECT pg_drop_replication_slot('slot_name')` to remove stale slots. In production, monitor `pg_replication_slots` and consider using a connection pooler that reuses slots.
The `event` filter is case-sensitive and uses the PostgreSQL event name: 'INSERT', 'UPDATE', 'DELETE', '*'. Many developers write `event: 'insert'` (lowercase) and wonder why it doesn't work.
The `table` filter is also case-sensitive and must match the exact table name. If your table is in a schema other than `public`, you must specify `schema: 'your_schema'`.
Frequently asked questions
Why do I see heartbeats but no data events?
Heartbeats indicate the WebSocket connection is alive. No data events mean the database changes are either not happening on a subscribed table/event, or they are being filtered out due to RLS or incorrect filters. Use the Realtime Inspector to confirm events are being emitted at the database level.
Do I need to enable Realtime for each table manually?
Yes. By default, no tables are in the `supabase_realtime` publication. You must add them via the Dashboard (Database → Realtime) or SQL: `ALTER PUBLICATION supabase_realtime ADD TABLE your_table;`. Changes take effect immediately.
Can I use Realtime with the `auth.users` table?
Yes, but you must specify `schema: 'auth'` in the subscription. Also note that RLS policies on `auth.users` are very restrictive; you may need to use the `service_role` key to receive events for that table.
What happens if my replication slots fill up?
PostgreSQL will stop creating new slots, and new subscriptions will fail. Monitor `pg_replication_slots` and set `max_replication_slots` to a value higher than your peak concurrent subscribers (default 10). Clean up unused slots with `pg_drop_replication_slot`.
Does Realtime work with Supabase's Row Level Security?
Yes, but the event is only sent if the subscriber's user has SELECT permission on the changed row. If the subscription uses an anon key, the event is sent only if there is an RLS policy allowing anon SELECT. Use the `service_role` key to bypass RLS entirely.