RowShield
Guides

Realtime authorisation and the policies people forget

Supabase Realtime ships three features under one name, and half the security confusion about it dissolves once they are separated. Postgres Changes streams database modifications and consults your Row Level Security policies per subscriber. Broadcast and Presence shuttle ephemeral messages through channels and never query your tables at all — RLS is simply not part of their story.

Which half a given feature belongs to determines which rules apply, and misassigning it in either direction produces the leaks and false confidence this page untangles. It is manual guidance: RowShield does not automate Realtime monitoring — no part of its scan reads publications or channel configuration.

RowShield does not detect this yet. This guide gives you the catalog queries to check it yourself. The nine rules that do ship are listed on the rules index.

Three features, two trust models

Postgres Changes replays INSERTs, UPDATEs and DELETEs from the database to subscribed clients, and it does so through the same authorization machinery as ordinary reads: a subscriber receives a change only if a SELECT policy on that table would admit them the changed row. Enable RLS on the table and change streams inherit its discipline; leave it off and the stream broadcasts to whoever subscribes, exactly as direct reads would.

Broadcast and Presence operate on a different plane entirely. Messages flow through channels without touching any table — cursors, typing indicators, transient state — so there is nothing for a policy to govern and RLS contributes nothing. Their authorization lives at channel access: who may join, who may send, decided where the channel is created or gated, not by the database. Channel names provide no security; assume any name is discoverable and gate accordingly.

The policy people forget

The recurring oversight runs backward from the expected one. Teams lock down REST reads carefully, then add a table to a publication months later and forget that the same SELECT policy now governs streaming delivery — broadening that policy to fix an app-wide listing quietly widens the change feed to match. The reverse surfaces too: a table kept deliberately restrictive for fetches surprises developers when subscribers see nothing, because the policy doing its fetch-time job is equally in force stream-time.

The habit that prevents both: whenever a table joins a publication, re-read its SELECT policies as if seeing them for the first time, asking specifically what they admit and to whom. Change delivery amplifies whatever those policies say, promptly and continuously, to every subscribed client — they are the entire authorization story for the stream.

-- The policy that governs BOTH fetches and change delivery:
CREATE POLICY "invoices_read_own"
  ON public.invoices
  FOR SELECT
  TO authenticated
  USING (owner_id = (SELECT auth.uid()));

Checking your own setup

A short ordered review covers the ground. Confirm RLS is enabled on every table feeding publications — change-stream governance exists only while that flag is on. Read the SELECT policies on those tables with streaming audiences in mind, noting any that admit broader access than your fetch paths intend. Treat every channel name as public knowledge and verify broadcast/presence channels carrying anything sensitive are gated by explicit authorization rather than obscurity. Finally, subscribe as a low-privilege test user and observe what arrives — behavioural truth beats configuration reading, because subscriptions cache and reconnect in ways static review misses.

SELECT schemaname, tablename, policyname, roles, qual
FROM pg_policies
WHERE tablename IN (
  SELECT tablename FROM pg_publication_tables
  WHERE pubname = 'supabase_realtime'
)
ORDER BY tablename, policyname;

Where monitoring fits

Realtime-specific configuration — publications, channel gating, presence settings — sits outside RowShield's scope today, and this page does not pretend otherwise. The database substrate underneath the stream is squarely in scope: the same scheduled scans verify RLS enabled, police SELECT policies for tautologies and missing WITH CHECK, and diff posture over time, so the policies that govern change delivery are watched as continuously as they are enforced. RowShield reads pg_catalog metadata only, never rows, and is an independent product, not affiliated with or endorsed by Supabase. Run a free audit at rowshield.dev/audit — no account required.

Frequently asked

Does disabling Realtime bypass Row Level Security?
No — Realtime is a reader of changes, governed by the same policies as any other reader while RLS is enabled. A table leaking through change streams is leaking through direct reads identically; the policy, not the transport, is the boundary.
Are private channels mandatory for Broadcast and Presence?
For any content a subscriber should not hear anonymously, some explicit gate is mandatory — RLS provides none, since no table is involved. Whether that gate is a private channel, a join authorization check server-side, or throwaway-only content is your architecture decision; obscurity of the channel name is not a gate.
Why did a client keep receiving changes after a policy change?
Existing subscriptions ride out reconnects and caching windows before new evaluations take full effect, so brief lag is normal. Persistent receipt after the lag points at a second, broader policy still admitting the client — check for overlapping permissive policies on the same table.

Check your project in about ten seconds

Paste a URL. No signup, no writes, nothing stored.

Run the free audit
supabase realtime rlsrealtime postgres_changes policysupabase private channels