Supabase Row Level Security Guide 2026
Master RLS policies in Supabase with production-ready patterns, real errors, and security best practices for indie hackers.
TL;DR
Supabase Row Level Security (RLS) restricts database access at the row level using PostgreSQL policies. Enable RLS on tables, create policies using auth.uid() and custom claims, and test with different user roles. Common pitfalls: forgetting to enable RLS, misconfigured auth.uid() checks, and policies blocking legitimate queries. Always verify policies in development before production.
---
What is Row Level Security?
Row Level Security (RLS) is PostgreSQL's built-in mechanism to control which rows users can access. With Supabase, you define policies that automatically enforce access rules without writing application logic. This shifts security from your app layer to the database—where it's harder to bypass.
Think of it this way: instead of filtering results in your JavaScript, the database itself refuses to return unauthorized rows.
Why RLS Matters
1. Defense in depth: Compromised app logic still can't access restricted data 2. Audit trail: PostgreSQL logs policy violations 3. Performance: Filtering happens at the database level, not in-app 4. Compliance: Satisfies regulations requiring data isolation (HIPAA, GDPR patterns)
---
Enable RLS on Your Table
First, you must explicitly enable RLS. By default, tables have no policies—anyone with table access can read everything.
```sql -- Connect as authenticated superuser (anon role cannot enable RLS) ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY; ```
Critical: Without explicit policies after enabling RLS, *no one* can access the table—not even authenticated users. This is the intended secure-by-default behavior.
---
Essential RLS Patterns
Pattern 1: Users Can Only See Their Own Data
```sql -- Create policy for SELECT CREATE POLICY "Users can view own posts" ON public.posts FOR SELECT USING (auth.uid() = user_id);
-- Create policy for INSERT CREATE POLICY "Users can create posts" ON public.posts FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Create policy for UPDATE CREATE POLICY "Users can update own posts" ON public.posts FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
-- Create policy for DELETE CREATE POLICY "Users can delete own posts" ON public.posts FOR DELETE USING (auth.uid() = user_id); ```
Key functions:
auth.uid(): Returns the current user's UUIDUSING: Applies to SELECT and DELETEWITH CHECK: Applies to INSERT and UPDATEPattern 2: Admin Access to All Rows
```sql CREATE POLICY "Admins can manage all posts" ON public.posts FOR ALL USING ( auth.jwt() ->> 'role' = 'admin' ) WITH CHECK ( auth.jwt() ->> 'role' = 'admin' ); ```
Verify your JWT claim name in [Supabase Auth configuration](/?guide=supabase-auth-jwt).
Pattern 3: Public Read, Authenticated Write
```sql -- Anyone can read published posts CREATE POLICY "Public can read published posts" ON public.posts FOR SELECT USING (is_published = true);
-- Only authenticated users can create CREATE POLICY "Authenticated users can create posts" ON public.posts FOR INSERT WITH CHECK ( auth.role() = 'authenticated' AND auth.uid() = user_id ); ```
---
Real Error Messages You'll Encounter
Error 1: Policy Violation on Select
``` ProgrammingError: new row violates row-level security policy "Users can view own posts" for table "posts" ```
Cause: Your RLS policy's USING clause evaluated to false.
Debug: Check that auth.uid() matches the user_id in the row.
Error 2: RLS Not Enabled
``` PostgresError: permission denied for schema public ```
Cause: RLS is enabled but no policies exist for the operation.
Fix: Add at least one policy for your use case.
Error 3: Auth Context Missing
``` ProgrammingError: invalid input syntax for type uuid: "null" ```
Cause: auth.uid() returned NULL because the request isn't authenticated.
Debug: Verify you're using a valid Supabase session token in your request headers.
---
Production-Ready Pattern: Complete Example
```typescript // Enable RLS on your table first (SQL) ALTER TABLE public.user_documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can CRUD own documents" ON public.user_documents FOR ALL USING (auth.uid() = owner_id) WITH CHECK (auth.uid() = owner_id);
CREATE POLICY "Admins can view all documents" ON public.user_documents FOR SELECT USING ( (auth.jwt() -> 'app_metadata' ->> 'role') = 'admin' ); ```
```typescript // Client-side usage (TypeScript) import { createClient } from '@supabase/supabase-js';
const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY );
// Authenticated request - RLS applies automatically const { data, error } = await supabase .from('user_documents') .select('*') .eq('status', 'active');
// Returns only documents where auth.uid() = owner_id // Database enforces this, not your app ```
---
Testing RLS in Development
```sql -- Test as specific user SET "request.jwt.claims" = '{"sub": "user-uuid-here", "email": "test@example.com"}';
SELECT * FROM public.posts; -- Returns only posts where user_id = 'user-uuid-here'
-- Clear the JWT context RESET "request.jwt.claims"; ```
For JavaScript testing, create clients with different auth tokens.
---
Common Mistakes to Avoid
1. Forgetting RLS on sensitive tables: Enable it first, then add policies
2. Using string comparison instead of auth.uid(): Always use UUID comparison for IDs
3. Policies that are too permissive: Start restrictive, expand carefully
4. Not testing with anon role: Your NEXT_PUBLIC_ANON_KEY should have minimal access
5. Assuming app-level filtering replaces RLS: It doesn't—RLS is your guardrail
---
Advanced: Custom Claims in JWT
Store team/organization IDs in auth claims:
```sql CREATE POLICY "Team members can see team posts" ON public.posts FOR SELECT USING ( team_id = (auth.jwt() -> 'app_metadata' ->> 'team_id')::uuid ); ```
Set custom claims in your [auth function](/?guide=supabase-custom-auth).
---
Verify in Official Docs
---
What am I missing?
Is your team using RLS in production? Found a gotcha this guide missed? Leave a comment with:
I'll update this guide based on real developer feedback.