Supabase Row Level Security 2026: Beginner Guide

Master Supabase RLS with production patterns, real errors, and step-by-step setup to secure your database at row level.

TL;DR

Row Level Security (RLS) in Supabase enforces database-level access control using PostgreSQL policies. Enable RLS on tables, create policies matching your auth logic, and test thoroughly—misconfigured policies cause silent data leaks or 42501 permission denied errors.

What is Row Level Security?

Row Level Security is a PostgreSQL feature that restricts which rows users can access based on database policies. Unlike application-level filtering, RLS policies execute *inside the database*, making them impossible to bypass through direct API calls or compromised client code.

Supabase integrates PostgreSQL's native RLS with its Auth system, allowing you to write policies that reference auth.uid() and user claims.

Enable RLS on Your Table

RLS is disabled by default. You must explicitly enable it:

```sql ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY; ```

Critical: Enabling RLS without policies blocks all access except the table owner. You'll immediately see:

``` Error: new row violates row-level security policy for table "posts" ```

This is expected—now create policies.

Core RLS Policies: READ, INSERT, UPDATE, DELETE

Each policy targets a specific operation. Here's a real-world example for a posts table:

```sql -- Allow users to SELECT their own posts + published posts CREATE POLICY "Users can view own or published posts" ON public.posts FOR SELECT USING ( auth.uid() = user_id OR is_published = true );

-- Allow users to INSERT only their own posts CREATE POLICY "Users can create posts" ON public.posts FOR INSERT WITH CHECK (auth.uid() = user_id);

-- Allow users to UPDATE only their own posts CREATE POLICY "Users can update own posts" ON public.posts FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);

-- Allow users to DELETE only their own posts CREATE POLICY "Users can delete own posts" ON public.posts FOR DELETE USING (auth.uid() = user_id); ```

Key distinction: USING applies to existing rows; WITH CHECK validates new/modified rows.

Real Console Errors You'll See

Error 1: Permission Denied (42501) ``` Error: permission denied for schema public Code: 42501 ``` Cause: No SELECT policy exists, or the policy condition returned false.

Error 2: Row-Level Security Violation ``` Error: new row violates row-level security policy for table "posts" Code: 20000 ``` Cause: INSERT/UPDATE WITH CHECK clause failed.

Error 3: Silent Data Loss ``` // Query succeeds but returns empty array—no error thrown const { data } = await supabase .from('posts') .select('*'); console.log(data); // [] ``` Cause: RLS filtering matched zero rows. Always test with known data.

Production Pattern: Role-Based Access

Use custom claims in JWT tokens for fine-grained control:

```sql -- Table: documents CREATE TABLE public.documents ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), title text NOT NULL, owner_id uuid NOT NULL REFERENCES auth.users(id), team_id uuid NOT NULL, created_at timestamp DEFAULT now() );

ALTER TABLE public.documents ENABLE ROW LEVEL SECURITY;

-- Policy: Users see docs from their team CREATE POLICY "team_members_can_view" ON public.documents FOR SELECT USING ( team_id = (auth.jwt() ->> 'user_metadata')::jsonb->>'team_id'::uuid );

-- Policy: Only owners can update CREATE POLICY "owners_can_update" ON public.documents FOR UPDATE USING (owner_id = auth.uid()) WITH CHECK (owner_id = auth.uid()); ```

Testing RLS Policies

Test policies in the SQL Editor with set role to simulate different users:

```sql -- Test as authenticated user SET ROLE authenticated; SET request.jwt.claims = '{"sub": "550e8400-e29b-41d4-a716-446655440000"}';

SELECT * FROM public.posts; -- Should return only rows matching policy

RESET ROLE; RESET request.jwt.claims; ```

In application code, switch users by using their actual auth tokens:

```javascript const { data: userPosts, error } = await supabase .from('posts') .select('*') .eq('user_id', userAId);

// Should succeed and return data if (error) console.error('RLS blocked:', error.code); ```

Common Mistakes

Mistake 1: Forgetting auth.uid() is null for anon ```sql -- WRONG: Anon users see nothing (auth.uid() is NULL) CREATE POLICY "public_view" ON public.posts FOR SELECT USING (auth.uid() = user_id);

-- RIGHT: Allow anon to see published posts CREATE POLICY "public_view" ON public.posts FOR SELECT USING ( is_published = true OR (auth.uid() IS NOT NULL AND auth.uid() = user_id) ); ```

Mistake 2: Cascading deletes without RLS If you delete a user, orphaned rows may violate policies. Use database triggers:

```sql CREATE TRIGGER cascade_delete_user_posts AFTER DELETE ON auth.users FOR EACH ROW EXECUTE FUNCTION delete_user_posts(); ```

Mistake 3: Over-permissive policies ```sql -- DANGEROUS: Grants access to all rows CREATE POLICY "allow_all" ON public.posts FOR SELECT USING (true); ```

Verify in Official Docs

Supabase RLS documentation is version-agnostic, but verify:

  • Current [Supabase Auth JWT claims structure](https://supabase.com/docs/guides/auth/jwt)
  • [PostgreSQL policy syntax](https://www.postgresql.org/docs/current/sql-createpolicy.html) (current version: verify in official docs)
  • [Supabase RLS best practices](https://supabase.com/docs/guides/database/postgres/row-level-security)
  • Related Guides

  • [Supabase Authentication Integration](/?guide=supabase-auth)
  • [PostgreSQL Triggers and Functions](/?guide=postgres-triggers)
  • What am I missing?

    Have you hit edge cases with RLS policies? Spotted inaccuracies in policy syntax? Drop corrections and real-world scenarios in comments—let's build a better resource for the indie hacker community.

    🔥 0d
    LIVE
    PlanetScale rage spiking Vercel pricing complaints Railway gaining fast Supabase happiness rising Resend loved by devs PlanetScale rage spiking Vercel pricing complaints Railway gaining fast Supabase happiness rising
    DEVELOPER PAIN RADAR // Loading...

    Developers complain.
    Opportunities appear.

    We track what developers are struggling with today — and what opportunities that creates.

    guides today
    avg happiness
    🔥 Pain
    📖 Guides
    🔭 Explore
    👤 Mine
    🔥 Pain Radar — rage scores today
    ↗ share
    💡 Opportunity Feed — pain = market gap
    📈 Tool Momentum
    all scores →
    📖 Latest Guide
    all guides →
    📖 All Guides
    📊 Tool Scores
    + Submit
    📰 Hacker News
    ➕ Submit a Tool
    ← back