Supabase RLS Not Working: Debug & Fix Policies (2026)
Master RLS debugging techniques. Learn how to identify, diagnose, and fix Row Level Security policy issues that block data access in production.
Debugging Supabase RLS Issues: A Step-by-Step Guide#
Row Level Security (RLS) is powerful but also a common source of frustration. You set up a policy, it works in development, then fails in production. Or you get cryptic "permission denied" errors with no clear cause.
This guide teaches you how to systematically debug RLS issues.
Understanding RLS Errors#
Common Error Messages#
"permission denied for schema public"
- RLS is enabled but no policy exists for this operation
- Solution: Create a policy for SELECT, INSERT, UPDATE, or DELETE
"new row violates row level security policy"
- INSERT or UPDATE failed because the new data violates the WITH CHECK condition
- Solution: Ensure the data matches the policy condition
"SELECT permission denied for table"
- RLS policy blocks SELECT access
- Solution: Check the USING condition in the SELECT policy
"UPDATE permission denied for table"
- RLS policy blocks UPDATE access
- Solution: Check the USING and WITH CHECK conditions
Step 1: Verify RLS is Enabled#
First, check if RLS is actually enabled on the table:
-- Check if RLS is enabled
SELECT tablename, rowsecurity
FROM pg_tables
WHERE tablename = 'posts';
-- Output:
-- tablename | rowsecurity
-- posts | t (true = enabled, f = false = disabled)If RLS is disabled, enable it:
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;Step 2: List All Policies#
See what policies exist on the table:
-- List all policies on a table
SELECT * FROM pg_policies
WHERE tablename = 'posts'
ORDER BY policyname;
-- Output shows:
-- policyname | cmd | qual | with_check
-- "Users can view own posts" | SELECT | (auth.uid() = user_id) | NULL
-- "Users can create posts" | INSERT | NULL | (auth.uid() = user_id)Key columns:
cmd: Operation (SELECT, INSERT, UPDATE, DELETE)qual: USING condition (for SELECT/DELETE)with_check: WITH CHECK condition (for INSERT/UPDATE)
Step 3: Test Policies with SQL#
Use the SQL Editor to test policies as different users:
-- Test as a specific user
SET ROLE authenticated;
SET request.jwt.claims = '{"sub":"user-id-123"}';
-- Try to select
SELECT * FROM posts;
-- If you get permission denied, the policy is blocking access
-- If you get results, the policy allows accessTesting Different Operations#
Test SELECT:
SET ROLE authenticated;
SET request.jwt.claims = '{"sub":"user-id-123"}';
SELECT * FROM posts;Test INSERT:
SET ROLE authenticated;
SET request.jwt.claims = '{"sub":"user-id-123"}';
INSERT INTO posts (title, content, user_id)
VALUES ('Test', 'Content', 'user-id-123');Test UPDATE:
SET ROLE authenticated;
SET request.jwt.claims = '{"sub":"user-id-123"}';
UPDATE posts
SET title = 'Updated'
WHERE id = 'post-id-123';Test DELETE:
SET ROLE authenticated;
SET request.jwt.claims = '{"sub":"user-id-123"}';
DELETE FROM posts WHERE id = 'post-id-123';Step 4: Debug Policy Conditions#
If a policy is blocking access, debug the condition:
-- Example policy
CREATE POLICY "Users can view own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
-- Debug: Check what auth.uid() returns
SELECT auth.uid();
-- Debug: Check the user_id in the data
SELECT id, user_id FROM posts LIMIT 5;
-- Debug: Check if they match
SELECT auth.uid() = user_id FROM posts LIMIT 5;Common Condition Issues#
Issue: auth.uid() is NULL
-- auth.uid() returns NULL if not authenticated
SELECT auth.uid(); -- Returns NULL
-- Solution: Make sure you're authenticated
-- In your app, check that the user is logged inIssue: Column doesn't exist
-- ❌ Bad: Column doesn't exist
CREATE POLICY "Users can view own posts"
ON posts FOR SELECT
USING (auth.uid() = owner_id); -- Column is user_id, not owner_id
-- ✅ Good: Use correct column name
CREATE POLICY "Users can view own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);Issue: Type mismatch
-- ❌ Bad: Comparing UUID to TEXT
CREATE POLICY "Users can view own posts"
ON posts FOR SELECT
USING (auth.uid()::text = user_id::text);
-- ✅ Good: Ensure types match
CREATE POLICY "Users can view own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id); -- Both are UUIDStep 5: Use EXPLAIN to Understand Query Plans#
Use EXPLAIN to see how PostgreSQL executes the query with RLS:
-- See the query plan with RLS
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM posts WHERE user_id = auth.uid();
-- Output shows:
-- Seq Scan on posts (cost=0.00..35.00 rows=1 width=100)
-- Filter: (user_id = auth.uid())
-- Rows Removed by Filter: 99This shows PostgreSQL is filtering rows based on the RLS condition.
Step 6: Test in Your Application#
After fixing the policy, test in your application:
// Test in your Next.js app
async function testRLS() {
const supabase = createClient();
// Get current user
const { data: { user } } = await supabase.auth.getUser();
console.log('Current user:', user?.id);
// Try to select
const { data, error } = await supabase
.from('posts')
.select('*');
if (error) {
console.error('RLS error:', error.message);
} else {
console.log('Success! Posts:', data);
}
}Common RLS Issues and Fixes#
Issue 1: Policy References Wrong Column#
-- ❌ Bad: Column name is wrong
CREATE POLICY "Users can view own posts"
ON posts FOR SELECT
USING (auth.uid() = owner_id); -- Should be user_id
-- ✅ Good: Use correct column
CREATE POLICY "Users can view own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);Issue 2: Missing Policy for Operation#
-- ❌ Bad: Only SELECT policy, no INSERT policy
CREATE POLICY "Users can view own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
-- User can SELECT but cannot INSERT!
-- ✅ Good: Add INSERT policy
CREATE POLICY "Users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);Issue 3: Complex Condition Fails#
-- ❌ Bad: Complex condition with typo
CREATE POLICY "Users can view organization posts"
ON posts FOR SELECT
USING (
organization_id IN (
SELECT organization_id FROM organization_members
WHERE user_id = auth.uid()
)
);
-- If organization_members table doesn't exist, this fails!
-- ✅ Good: Verify table and columns exist
CREATE POLICY "Users can view organization posts"
ON posts FOR SELECT
USING (
organization_id IN (
SELECT organization_id FROM organization_members
WHERE user_id = auth.uid()
)
);
-- Test the subquery separately
SELECT organization_id FROM organization_members
WHERE user_id = auth.uid();Issue 4: RLS Blocks Realtime#
-- ❌ Bad: RLS policy blocks realtime subscriptions
CREATE POLICY "Users can view own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
-- Realtime subscriptions fail silently!
-- ✅ Good: Test RLS with SELECT first
SELECT * FROM posts WHERE auth.uid() = user_id;
-- If SELECT works, realtime will workIssue 5: Multi-Tenant Access Issues#
-- ❌ Bad: Doesn't check organization membership
CREATE POLICY "Users can view organization posts"
ON posts FOR SELECT
USING (organization_id = current_setting('app.organization_id')::uuid);
-- If organization_id setting is not set, this fails!
-- ✅ Good: Check organization membership
CREATE POLICY "Users can view organization posts"
ON posts FOR SELECT
USING (
organization_id IN (
SELECT organization_id FROM organization_members
WHERE user_id = auth.uid()
)
);RLS Debugging Checklist#
- ✅ RLS is enabled on the table
- ✅ Policy exists for the operation (SELECT, INSERT, UPDATE, DELETE)
- ✅ Policy condition references correct columns
- ✅ Column types match (UUID = UUID, not UUID = TEXT)
- ✅ auth.uid() returns the correct user ID
- ✅ Policy condition is logically correct
- ✅ Subqueries in policy return expected results
- ✅ User is authenticated (not NULL)
- ✅ Test with production data and user IDs
- ✅ Realtime subscriptions work (if using realtime)
Debugging Tools#
Supabase Dashboard#
- Go to SQL Editor
- Run test queries with different user IDs
- Check Database → Logs for permission denied errors
- View policies in Database → Policies
Application Logging#
// Log RLS errors in your app
async function debugRLS() {
const supabase = createClient();
const { data, error } = await supabase
.from('posts')
.select('*');
if (error) {
console.error('RLS Error:', {
message: error.message,
code: error.code,
details: error.details
});
}
}PostgreSQL Logs#
Enable query logging to see RLS policy evaluations:
-- Enable query logging
ALTER SYSTEM SET log_statement = 'all';
ALTER SYSTEM SET log_min_duration_statement = 0;
-- Reload configuration
SELECT pg_reload_conf();
-- View logs
SELECT * FROM pg_read_file('postgresql.log', 0, 1000000);Best Practices#
- ✅ Test policies thoroughly before production
- ✅ Use simple, clear policy conditions
- ✅ Document complex policies with comments
- ✅ Test with production data and user IDs
- ✅ Monitor logs for permission denied errors
- ✅ Use EXPLAIN to understand query plans
- ✅ Break complex policies into smaller parts
- ✅ Test all operations: SELECT, INSERT, UPDATE, DELETE
Related Articles#
- Security Best Practices
- Building SaaS with Next.js and Supabase
- 10 Common Mistakes Building with Next.js and Supabase
- Supabase vs Firebase in 2026: The Honest Comparison
- Supabase vs Testing Library: 2026 Strategy
- Jest vs Supabase: Mocking vs Integration in 2026
- Supabase RLS Policy Not Working: 2026 Debug Checklist
- Fix Supabase RLS Infinite Recursion
- Debug Supabase RLS Silent Failures
Conclusion#
RLS debugging requires systematic thinking. Start by verifying RLS is enabled, list all policies, test conditions with SQL, and use EXPLAIN to understand query plans. Most RLS issues stem from simple mistakes: wrong column names, missing policies, or incorrect conditions.
With these debugging techniques, you'll quickly identify and fix RLS issues. Remember: test thoroughly in development before deploying to production.
Frequently Asked Questions
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 RLS Policies Silently Failing: 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.
Fix Supabase RLS Infinite Recursion (Production 2026)
If your Supabase query returns `infinite recursion detected in policy for relation "X"`, your RLS policy is querying the same table it protects. Here's exactly why it loops, and three production-grade fixes that don't leak data.
Supabase bucket RLS policy for table objects fix
This Storage error almost never means Supabase is broken. It usually means your upload path, your RLS policy, or your use of `upsert` does not match how Storage actually authorizes writes.
Browse by Topic
Find stories that matter to you.
