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.
Why Your Supabase RLS Policies Are Silently Failing (And How to Debug Them)#
You write a query. It returns nothing. No error, no warning — just an empty array.
You check the data. It's there. You check your query. It's correct. You spend 45 minutes before realizing: RLS is filtering out every row because your policy has a bug.
This is the most frustrating thing about Row Level Security. It doesn't fail loudly. It silently returns nothing, and your application looks broken for reasons that have nothing to do with your application code.
Here's the debugging workflow I use every time.
Step 1: Confirm RLS Is the Problem#
First, rule out everything else. Run this in the Supabase SQL editor:
-- Temporarily bypass RLS to confirm data exists
SELECT * FROM your_table LIMIT 10;
The SQL editor runs as the postgres superuser by default, which bypasses RLS. If you see data here but not in your app, RLS is the culprit.
If you want to simulate your app's query exactly:
-- Simulate an authenticated user
SET LOCAL role = authenticated;
SET LOCAL "request.jwt.claims" = '{"sub": "your-actual-user-uuid"}';
SELECT * FROM your_table LIMIT 10;
If this returns nothing but the superuser query returns data, you've confirmed the policy is filtering your rows.
Step 2: Check Which Policies Are Active#
SELECT
schemaname,
tablename,
policyname,
permissive,
roles,
cmd,
qual,
with_check
FROM pg_policies
WHERE tablename = 'your_table';
Look at the qual column — that's the USING expression. Look at with_check — that's the WITH CHECK expression. Read them carefully. The bug is usually obvious once you see the raw SQL.
Common things to look for:
auth.uid() = user_id— isuser_idactually the column name in your table?org_id = $1— is there a missing join or subquery?- A policy that only covers
SELECTbut notINSERT, leaving inserts silently blocked
Step 3: The Most Common Bugs#
Wrong column name. Your policy says user_id but your column is created_by or owner_id. Returns empty, no error.
NULL comparison. auth.uid() = user_id evaluates to NULL when user_id is NULL. NULL is not true, so the row is filtered out. If you have rows with NULL user_id, they're invisible to everyone.
Missing policy for the operation. You have a SELECT policy but no INSERT policy. Inserts return no error — they just silently do nothing. Check cmd in pg_policies to see which operations are covered.
Multiple permissive policies with unexpected OR behavior. Two FOR SELECT policies on the same table are OR'd together. If one is too permissive, it overrides the other. If one is too restrictive, the other might still let rows through. This is often surprising.
Using auth.jwt() claims that are stale. If you store roles in JWT claims, a user whose role changed won't see the update until their token expires. Their old role is still in the claim, and your policy is evaluating the old value.
Step 4: Test Each Policy in Isolation#
For complex policies with subqueries, test the subquery directly:
SET LOCAL role = authenticated;
SET LOCAL "request.jwt.claims" = '{"sub": "your-user-uuid"}';
-- Test the subquery from your policy
SELECT EXISTS (
SELECT 1 FROM org_members
WHERE org_members.org_id = 'your-org-id'
AND org_members.user_id = auth.uid()
);
If this returns false when you expect true, the problem is in your membership data, not your policy logic.
Step 5: Check the Service Role Bypass#
If your Next.js app uses the service role key (SUPABASE_SERVICE_ROLE_KEY), RLS is bypassed entirely. This is intentional for admin operations, but it means you can't test RLS behavior using the service role client.
To test RLS from your app, temporarily use the anon key client:
// Temporary debug client — don't use in production
const debugClient = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
If this returns different results than your service role client, RLS is doing something you didn't expect.
Step 6: Use EXPLAIN to Check Performance#
Once your policies work correctly, check that they're not killing performance:
EXPLAIN ANALYZE
SELECT * FROM projects WHERE org_id = 'your-org-id';
Look for Seq Scan on large tables. If your policy uses a subquery without an index, every row evaluation triggers a full table scan. Add indexes on columns used in policy conditions:
CREATE INDEX idx_org_members_user_id ON org_members(user_id);
The Debugging Checklist#
When RLS returns empty results:
- Run the query as superuser in SQL editor — does data exist?
- Run with
SET LOCAL role = authenticated— does it disappear? - Check
pg_policies— are the right policies active for the right operations? - Read the
qualexpression — does the column name match your schema? - Test subqueries in isolation — does the membership/role lookup return what you expect?
- Check for NULL values in columns used in policy conditions
- Verify you're not accidentally using the service role key in your test
RLS bugs are almost always one of these six things. Work through the list and you'll find it.
The deeper lesson: treat RLS like application code. Write it in small pieces, test each piece in isolation, and use the SQL editor's role simulation before deploying. Silent failures are only silent if you're not looking in the right place.
Have a specific RLS pattern you're struggling with? [INTERNAL LINK: supabase-rls-policy-design-patterns] covers the advanced patterns in depth.
Continue Reading
Debugging Supabase RLS Issues: A Step-by-Step Guide
Master RLS debugging techniques. Learn how to identify, diagnose, and fix Row Level Security policy issues that block data access in production.
10 Common Mistakes Building with Next.js and Supabase (And How to Fix Them)
Avoid these critical mistakes when building with Next.js and Supabase. Learn from real-world errors that cost developers hours of debugging and discover proven solutions.
The Supabase Auth Pattern That Saved My Startup From a $50K Security Audit Failure
How we went from failing enterprise security requirements to passing SOC 2 compliance in 6 weeks. The authentication architecture patterns that actually work at scale.
Browse by Topic
Find stories that matter to you.