Supabase Row Level Security: Beginner's Guide 2026
Master Supabase RLS policies with production-ready patterns. Learn auth integration, common 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, define policies tied to auth.uid(), and test thoroughly—incorrect policies create silent data leaks or API errors like 42501 permission denied.
Why Row Level Security Matters
Without RLS, your API endpoint /api/posts returns all posts to all users. RLS ensures users only access their own data or publicly shared rows. This is the difference between a hobby project and production-ready SaaS.
Supabase handles authentication via JWT tokens containing the user's UUID. Every database query automatically includes this context, making RLS policies aware of *who* is querying.
Core Concepts
Authentication Context
When a user authenticates with Supabase Auth, they receive a JWT containing: ```json { "sub": "550e8400-e29b-41d4-a716-446655440000", "aud": "authenticated", "role": "authenticated" } ```
In your RLS policies, you access this via:
auth.uid() — returns the user's UUIDauth.jwt() — returns the full JWT payloadauth.role() — returns 'authenticated' or 'anon'Policy Actions
Policies control four operations:
You can define separate policies per action.
Step 1: Enable RLS on Your Table
Create a table and immediately enable RLS:
```sql CREATE TABLE posts ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, title TEXT NOT NULL, content TEXT NOT NULL, is_public BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() );
ALTER TABLE posts ENABLE ROW LEVEL SECURITY; ```
⚠️ Critical: After ALTER TABLE posts ENABLE ROW LEVEL SECURITY, no policies exist yet—*all access is denied* (including your own). You must immediately create policies or you'll see:
``` ERROR: new row violates row-level security policy for table "posts" (SQLSTATE 42601) ```
Step 2: Create Your First Policy
Policy 1: Users Read Their Own Posts
```sql CREATE POLICY "Users can read own posts" ON posts FOR SELECT USING (auth.uid() = user_id); ```
This policy says: "A user can SELECT a row if their auth.uid() matches the user_id column."
Policy 2: Users Create Posts (Linked to Their Account)
```sql CREATE POLICY "Users can create posts" ON posts FOR INSERT WITH CHECK (auth.uid() = user_id); ```
The WITH CHECK clause validates the incoming data. If someone tries to insert a post with a different user_id, Postgres rejects it.
Policy 3: Users Update Only Their Own Posts
```sql CREATE POLICY "Users can update own posts" ON posts FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); ```
Note: USING controls which rows can be updated; WITH CHECK validates the new values.
Policy 4: Users Delete Only Their Own Posts
```sql CREATE POLICY "Users can delete own posts" ON posts FOR DELETE USING (auth.uid() = user_id); ```
Step 3: Handle Public Data
Many apps show public content to everyone. Add this policy:
```sql CREATE POLICY "Anyone can read public posts" ON posts FOR SELECT USING (is_public = TRUE OR auth.uid() = user_id); ```
Now anonymous users and authenticated users can see public posts, and authenticated users also see their own.
Real-World Error Messages
Error 1: Permission Denied on Insert
``` 42501: new row violates row-level security policy "Users can create posts" for table "posts" DETAIL: Policy with check expression "(auth.uid() = user_id)" was violated. ```
Cause: Your WITH CHECK policy rejected the data. Check that user_id in the INSERT matches auth.uid().
Error 2: Silent Empty Results
```javascript const { data, error } = await supabase .from('posts') .select('*');
console.log(data); // [] ```
Cause: Your SELECT policy is too restrictive. If you get [] instead of an error, the policy denied access silently. Verify your USING condition.
Error 3: Policy Doesn't Exist
``` 42P01: relation "auth.users" does not exist DETAIL: Policy references "auth.users" but table is in wrong schema. ```
Cause: In local development with supabase start, ensure you're running migrations that create the auth schema.
Production Patterns
Pattern 1: Organization-Based Access
```sql CREATE TABLE documents ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, org_id BIGINT NOT NULL REFERENCES organizations(id), created_by UUID NOT NULL REFERENCES auth.users(id), content TEXT );
CREATE TABLE org_members ( org_id BIGINT NOT NULL REFERENCES organizations(id), user_id UUID NOT NULL REFERENCES auth.users(id), PRIMARY KEY (org_id, user_id) );
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users see docs from their orgs" ON documents FOR SELECT USING ( org_id IN ( SELECT org_id FROM org_members WHERE user_id = auth.uid() ) ); ```
Pattern 2: Custom Claims from JWT
If you store custom data in the JWT:
```sql CREATE POLICY "Check user role" ON admin_logs FOR SELECT USING (auth.jwt() ->> 'role' = 'admin'); ```
Verify in [official Supabase JWT docs](https://supabase.com/docs/guides/auth/jwt).
Testing Your Policies
Use the Supabase Dashboard's "SQL Editor" to test:
```sql -- Simulate authenticated user SET request.jwt.claim.sub = '550e8400-e29b-41d4-a716-446655440000';
SELECT * FROM posts; ```
Or test via client:
```javascript const { data, error } = await supabase .from('posts') .insert({ user_id: auth.user().id, title: 'Test', content: 'Hello' }) .select();
if (error) console.error('RLS violation:', error.code); ```
Common Pitfalls
1. Forgetting ON DELETE CASCADE: If you delete a user, orphaned rows may bypass RLS.
2. Policies without indexes: auth.uid() = user_id needs an index on user_id for performance.
3. Testing without logging in: Anonymous access has auth.uid() = NULL, which fails most policies.
Further Reading
Related guides: [Authentication patterns](/?guide=supabase-auth), [Database indexing for RLS performance](/?guide=postgres-indexes)
What am I missing?
RLS is nuanced—edge cases abound. What tripped you up? Missing patterns for team workflows, multi-tenant apps, or audit logging? Drop a comment below with your hardest RLS problem, and let's solve it together.