Supabase Row Level Security Guide 2026

Master RLS policies to secure your PostgreSQL data. Production-ready patterns, common errors, and exact implementation steps.

TL;DR

Supabase Row Level Security (RLS) enforces database-level access control automatically. Enable it on tables, write policies defining who sees what, and test thoroughly—it's the foundation of secure multi-tenant apps. Verify current policy limits in [official Supabase docs](https://supabase.com/docs/guides/auth/row-level-security).

What is Row Level Security?

RLS is PostgreSQL's built-in feature that filters database rows before they're returned to your application. Instead of handling authorization in code, the database enforces it—eliminating security bugs from forgotten checks.

Why this matters:

  • Database-enforced (can't bypass it from app code)
  • Automatic for all queries (REST, RealtimeAPI, direct SQL)
  • Scales cleanly with [multi-tenant SaaS](/?guide=multi-tenant-architecture)
  • Single source of truth for access rules
  • Enable RLS on Your Table

    RLS is disabled by default. First, turn it on:

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

    Now *no one* can read, insert, or update rows—even as the table owner—until you create explicit policies. This is intentionally restrictive.

    Core Policy Pattern: Owner-Based Access

    Most apps follow this: users own their data, and RLS ensures they only see their own rows.

    ```sql -- Create a policy allowing users to read only their own documents CREATE POLICY "Users can read their own documents" ON public.documents FOR SELECT USING (auth.uid() = user_id);

    -- Create a policy allowing users to insert their own documents CREATE POLICY "Users can insert their own documents" ON public.documents FOR INSERT WITH CHECK (auth.uid() = user_id);

    -- Create a policy allowing users to update their own documents CREATE POLICY "Users can update their own documents" ON public.documents FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);

    -- Create a policy allowing users to delete their own documents CREATE POLICY "Users can delete their own documents" ON public.documents FOR DELETE USING (auth.uid() = user_id); ```

    Key components:

  • auth.uid() – Supabase function returning the authenticated user's UUID
  • USING clause – filters which rows can be accessed
  • WITH CHECK – filters which rows can be modified (INSERT/UPDATE)
  • FOR SELECT/INSERT/UPDATE/DELETE – operation-specific policies
  • Team-Based Access (Shared Workspaces)

    For collaborative apps where users belong to teams:

    ```sql -- Assume tables: teams, team_members, documents

    CREATE POLICY "Users can read docs in their teams" ON public.documents FOR SELECT USING ( EXISTS ( SELECT 1 FROM public.team_members WHERE team_members.team_id = documents.team_id AND team_members.user_id = auth.uid() ) );

    CREATE POLICY "Users can insert docs in their teams" ON public.documents FOR INSERT WITH CHECK ( EXISTS ( SELECT 1 FROM public.team_members WHERE team_members.team_id = documents.team_id AND team_members.user_id = auth.uid() ) ); ```

    This pattern scales to any membership model—roles, permissions, hierarchies, etc.

    Real Error Messages You'll See

    Error 1: Policy prevents all access ``` PEQ: new row violates row-level security policy "Users can read their own documents" for table "documents" ``` This means your USING or WITH CHECK clause evaluated to false. Check that auth.uid() matches the row's user_id.

    Error 2: No policies exist ``` ERROR: permission denied for table documents ``` You enabled RLS but forgot to create any policies. The database blocks all access.

    Error 3: Recursive policy check ``` ERROR: infinite recursion detected in policy for relation "documents" ``` A policy references the same table in its subquery without proper conditions. Add NOT MATERIALIZED or restructure the logic.

    Testing Policies (Critical)

    Never deploy RLS changes without testing. Use Supabase's SQL editor:

    ```sql -- Test as a specific user SET request.jwt.claim.sub = 'user-uuid-here'; SELECT * FROM documents; -- Should return only that user's rows

    -- Reset to bypass RLS (superuser) RESET request.jwt.claim.sub; SELECT * FROM documents; -- Should return all rows ```

    Or test from your app:

    ```javascript const { data, error } = await supabase .from('documents') .select('*');

    if (error?.code === 'PGRST116') { console.error('RLS policy denied access:', error.message); } else { console.log('User can access:', data); } ```

    Common Pitfalls

    1. Forgetting to enable RLS on joined tables If documents reference projects, enable RLS on *both* tables and write policies for each.

    2. Over-complex policies Avoid deeply nested subqueries. Use [materialized views](/?guide=postgresql-views) for complex checks.

    3. Assuming RLS catches everything RLS controls *which rows* users see—it doesn't encrypt data or hide column names. Use column-level security for sensitive fields.

    4. Not testing with real JWT tokens The Supabase dashboard bypasses RLS. Always test client-side with actual authentication.

    Performance Notes

    RLS adds overhead on every query. For tables with >100K rows and complex policies:

  • Create indexes on join columns (user_id, team_id, etc.)
  • Monitor query execution with EXPLAIN ANALYZE
  • Consider denormalization if policies cause full table scans
  • Verify performance expectations in [Supabase performance docs](https://supabase.com/docs/guides/database/performance-tuning).

    Anon Key Limitations

    Supabase provides two keys:

  • Anon key: No authenticated user. RLS checks auth.uid() IS NULL
  • Service role key: Bypasses RLS entirely (use server-side only)
  • Never expose the service role key. Use it only in backend code.

    Production Checklist

  • [ ] RLS enabled on all user-facing tables
  • [ ] Policies tested with multiple user accounts
  • [ ] Service role key stored as environment variable, never in frontend
  • [ ] Audit logs enabled for RLS changes
  • [ ] Documented which policies apply to each table
  • [ ] Performance tested with production data volume
  • What am I missing?

    Did I overlook a critical RLS pattern? Encountered an error message not covered? Have questions about integrating RLS with [authentication flows](/?guide=supabase-auth-patterns)? Drop corrections and additions in the comments—this guide improves with community input.

    Resources:

  • [Supabase RLS Official Guide](https://supabase.com/docs/guides/auth/row-level-security)
  • [PostgreSQL RLS Docs](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
  • [RLS Best Practices](https://supabase.com/docs/guides/auth)
  • 🔥 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