Supabase Row Level Security: Beginner Guide 2026

Learn to implement RLS policies in Supabase with production-ready patterns. Avoid common pitfalls and secure your PostgreSQL database properly.

TL;DR

Supabase Row Level Security (RLS) enforces database-level access control using PostgreSQL policies. Enable RLS on tables, create policies matching your auth logic, and test thoroughly before production. Common mistakes: forgetting USING clauses, not handling null auth tokens, and testing without SET role context.

---

What is Row Level Security?

Row Level Security is a PostgreSQL feature that restricts which rows users can access based on policies you define. In Supabase, RLS becomes your source-of-truth security layer—even if your frontend is compromised, unauthorized users can't read or modify protected data.

Think of it as middleware at the database level, not the application level. When a user runs a query, PostgreSQL automatically filters results based on policies attached to their role.

Current stable version: Supabase v2.x uses PostgreSQL 15.x (verify in official docs for latest patch)

---

Enable RLS on Your Table

First, enable RLS on tables that need protection:

```sql ALTER TABLE posts ENABLE ROW LEVEL SECURITY; ALTER TABLE comments ENABLE ROW LEVEL SECURITY; ```

Critical: Once RLS is enabled with no policies, ALL queries are blocked (even SELECT). This is intentional—you must explicitly grant access.

---

Create Your First Policy

Let's secure a posts table so users see only their own posts:

```sql -- Policy: Users can SELECT their own posts CREATE POLICY "Users can view own posts" ON posts FOR SELECT USING (auth.uid() = user_id);

-- Policy: Users can INSERT posts (they own) CREATE POLICY "Users can insert own posts" ON posts FOR INSERT WITH CHECK (auth.uid() = user_id);

-- Policy: Users can UPDATE their own posts CREATE POLICY "Users can update own posts" ON posts FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);

-- Policy: Users can DELETE their own posts CREATE POLICY "Users can delete own posts" ON posts FOR DELETE USING (auth.uid() = user_id); ```

Key components:

  • USING clause: Applied to SELECT/DELETE; determines which rows user sees
  • WITH CHECK clause: Applied to INSERT/UPDATE; validates the new data
  • auth.uid(): Supabase function returning authenticated user's UUID
  • ---

    Common Error Messages & Solutions

    Error 1: Policy Prevents All Access

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

    Cause: Your WITH CHECK condition failed during INSERT/UPDATE.

    Fix: Ensure the condition matches your actual data: ```sql -- Bad: tries to set user_id to someone else const { data, error } = await supabase .from('posts') .insert([{ title: 'Hello', user_id: 'other-uuid' }]);

    -- Good: user_id from auth context const { data, error } = await supabase .from('posts') .insert([{ title: 'Hello', user_id: (await supabase.auth.getUser()).data.user.id }]); ```

    Error 2: NULL Auth Token

    ``` PostgresError: row level security policy "Users can view own posts" is violated ```

    Cause: auth.uid() returned NULL because request lacked auth header.

    Fix: Check client initialization: ```typescript // Ensure Supabase client reads auth token import { createClient } from '@supabase/supabase-js';

    const supabase = createClient( process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY );

    // Verify token exists const { data: { session } } = await supabase.auth.getSession(); console.log(session?.access_token); // Should not be null ```

    Error 3: Public Read Policy Blocks Anonymous Access

    ``` PostgresError: permission denied for schema public ```

    Cause: No policy allows anonymous users to read public data.

    Fix: Add a public read policy: ```sql CREATE POLICY "Public posts are readable" ON posts FOR SELECT USING (is_public = true); ```

    ---

    Production-Ready Patterns

    Pattern 1: Role-Based Access

    For admin-only operations:

    ```sql -- Create custom claims in auth.users -- Verify in official docs: [Supabase Auth](https://supabase.com/docs/guides/auth/managing-user-data)

    CREATE POLICY "Admins can delete any post" ON posts FOR DELETE USING ( (SELECT raw_user_meta_data->>'role' FROM auth.users WHERE id = auth.uid()) = 'admin' ); ```

    Pattern 2: Shared Access via Foreign Keys

    Allow access to resources shared with the user:

    ```sql CREATE TABLE projects ( id uuid PRIMARY KEY, name text, owner_id uuid REFERENCES auth.users(id) );

    CREATE TABLE project_members ( project_id uuid REFERENCES projects(id), member_id uuid REFERENCES auth.users(id), PRIMARY KEY (project_id, member_id) );

    CREATE POLICY "Users see projects they own or are members of" ON projects FOR SELECT USING ( owner_id = auth.uid() OR EXISTS ( SELECT 1 FROM project_members WHERE project_members.project_id = projects.id AND project_members.member_id = auth.uid() ) ); ```

    Pattern 3: Testing RLS Policies

    Always test with proper role context:

    ```sql -- In psql, simulate authenticated user SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claims" = '{"sub":"user-uuid-here"}'; SELECT * FROM posts; -- Should respect policy

    -- Test as anon (no auth) SET LOCAL ROLE anon; SELECT * FROM posts; -- Should be blocked ```

    ---

    Testing in Your Application

    ```typescript // Test that user can't bypass RLS via raw SQL const { data, error } = await supabase .rpc('check_post_access', { post_id: 'some-id' }); // RLS still applies to RPC functions

    // Use service role ONLY in trusted backend // Never expose service_role_key to frontend const adminClient = createClient( process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY // Backend only! ); ```

    ---

    Debugging Checklist

  • [ ] RLS enabled on table: SELECT tablename FROM pg_tables WHERE schemaname = 'public';
  • [ ] At least one policy exists: SELECT * FROM pg_policies WHERE tablename = 'posts';
  • [ ] Auth token included in request headers
  • [ ] auth.uid() matches actual user UUID column
  • [ ] No typos in role/claim names
  • [ ] Tested with correct SET ROLE context
  • Verify in [official docs](https://supabase.com/docs/guides/database/postgres/row-level-security) for latest syntax and gotchas.

    ---

    Related Resources

  • [RLS Performance Optimization](/?guide=supabase-rls-performance)
  • [Securing Realtime Subscriptions](/?guide=supabase-realtime-security)
  • ---

    What am I missing?

    Have you hit edge cases with RLS? Found better patterns for team-based access? Spotted inaccuracies in this guide? Drop a comment below—experienced indie hackers reading this will appreciate real-world gotchas and solutions.

    🔥 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