Supabase queries return empty after enabling RLS
Lists and dashboards that worked in development render empty in production. The table has rows in the Supabase dashboard, the network tab shows 200 responses — but every select comes back with zero rows.
- Supabase
- Lovable
- Bolt
- Auth & login
Root cause, in plain English
Row Level Security denies by default. The moment RLS is enabled on a table, every query through the anon or authenticated role must be allowed by an explicit policy — and a select blocked by RLS doesn't error, it just returns zero rows. Development usually worked because RLS was off or queries ran with the service-role key, which bypasses policies entirely. Policies built on auth.uid() also return nothing for requests that aren't actually signed in.
How to fix it
In the Supabase dashboard, open Authentication → Policies (or Table Editor → the table) and list which policies exist for the table that's coming back empty. "RLS enabled, no policies" means nobody can read it.
Write an explicit SELECT policy for the role your app uses — e.g. USING (auth.uid() = user_id) for per-user data, or USING (true) for genuinely public rows.
Test as the app, not as yourself: the dashboard's SQL editor runs as an admin role. Use the API with the anon key, or impersonate a role in the SQL editor, to see what your users see.
Confirm the request is actually authenticated — a missing or expired JWT silently downgrades you to anon, and auth.uid() becomes null.
Never "fix" it by shipping the service_role key to the browser. It bypasses every policy and is equivalent to publishing your database password.
How Nightlamp detects this automatically
- API canary
- Keyword check
- Browser journey
An api_canary queries a known-good endpoint and asserts the response actually contains data — a 200 with an empty array trips the alert, which is exactly the failure shape RLS produces. An http_keyword check asserts a known record renders on the page, and a browser_journey signs in and verifies the dashboard shows real rows.
Catch this before your customers do
Nightlamp runs these checks continuously against your live app and sends a plain-English diagnosis — not a wall of logs — the moment this pattern shows up.
Related patterns
Frequently asked questions
- Why doesn't a blocked query return an error?
- By design, RLS filters rows rather than rejecting queries: rows you can't see simply aren't in the result. That's good for security (no information leak about hidden rows) but terrible for debugging, because an over-restrictive policy looks identical to an empty table.
- Should I just disable RLS to unblock production?
- Only if the table is truly public. With RLS off, anyone holding your anon key — which ships in your frontend bundle — can read and write the whole table. Write the correct policy instead; it's usually one line.
- Inserts work but reads come back empty. How?
- Policies are per-operation. An INSERT policy without a matching SELECT policy produces exactly this: writes succeed, then the app can't read back what it wrote. Check that each operation your app performs has its own policy.
- Why did this only break when I published from my AI builder?
- Builders like Lovable and Bolt often enable RLS on tables they create (a good default) or your preview ran against a different project where RLS was off. Publishing switched the app to the production project, where the policies don't match what the app needs.
Newsletter
Get new incident patterns as we publish them
One email when new failure patterns, fixes, and monitoring recipes for no-code and AI-built apps land. No fluff, unsubscribe any time.
Double opt-in. One-click unsubscribe. No spam, ever.