Supabase Row Level Security: Beginner's Guide 2026

Learn RLS policies in Supabase with production-ready patterns, real error messages, and exact syntax to secure your PostgreSQL data.

TL;DR

Supabase Row Level Security (RLS) enforces database-level access control via PostgreSQL policies. Enable it per table, write policies matching your auth users to rows, and test thoroughly—misconfigured policies block all queries with cryptic errors.

---

What is Row Level Security?

Row Level Security is a PostgreSQL feature that restricts which rows a user can query, insert, update, or delete based on policies you define. Supabase exposes this through the dashboard and SQL, letting you move security logic from your application to the database.

Why it matters:

  • Prevents accidental data leaks if your app logic has bugs
  • Scales security with your data instead of request handlers
  • Works across any client connecting via Supabase auth tokens
  • ---

    Enable RLS on a Table

    RLS is disabled by default. Navigate to your table in the Supabase dashboard:

    1. Go to Authentication → Policies 2. Select your table 3. Click Enable RLS

    Or via SQL (PostgreSQL 9.5+):

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

    ⚠️ Critical: Enabling RLS without policies blocks all queries for authenticated users. You'll see:

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

    If using the Anon role (unauthenticated), you'll get:

    ``` ERROR: permission denied for schema public ```

    ---

    Core Concepts

    Auth Context

    Supabase injects user data via auth.uid() (JWT subject) and auth.jwt() (full JWT claims). Use these in policy conditions:

    ```sql -- Access user's own posts CREATE POLICY "Users can view own posts" ON public.posts FOR SELECT USING (auth.uid() = user_id); ```

    Policy Anatomy

    ```sql CREATE POLICY policy_name ON table_name FOR operation -- SELECT, INSERT, UPDATE, DELETE, or ALL TO role_name -- authenticated, anon, or custom role USING (expression) -- For SELECT/UPDATE/DELETE WITH CHECK (expression); -- For INSERT/UPDATE ```

    USING: Filters which rows the user sees (SELECT, DELETE, UPDATE read-side) WITH CHECK: Validates data before insert/update (write-side)

    ---

    Real-World Patterns

    Pattern 1: Personal Data

    Users access only their own records:

    ```sql CREATE POLICY "Users can manage own profile" ON public.profiles FOR ALL USING (auth.uid() = id) WITH CHECK (auth.uid() = id); ```

    Pattern 2: Public + Private Rows

    Everyone reads public posts; owners modify theirs:

    ```sql CREATE POLICY "Posts are viewable by everyone" ON public.posts FOR SELECT USING (is_public = true OR auth.uid() = user_id);

    CREATE POLICY "Users can modify own posts" ON public.posts FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); ```

    Pattern 3: Team-Based Access

    Users access rows where they're team members (requires junction table):

    ```sql CREATE POLICY "Team members can view projects" ON public.projects FOR SELECT USING ( EXISTS ( SELECT 1 FROM public.team_members WHERE team_id = projects.team_id AND user_id = auth.uid() ) ); ```

    Pattern 4: Admin Bypass

    Admins see everything (custom JWT claim):

    ```sql CREATE POLICY "Admins can see all" ON public.posts FOR SELECT USING ( auth.uid() = user_id OR (auth.jwt() ->> 'role') = 'admin' ); ```

    Verify your JWT includes custom claims in [Supabase Auth docs](https://supabase.com/docs/guides/auth/jwt).

    ---

    Common Errors & Debugging

    Error 1: Policy Silently Denies Query

    ``` ERROR: new row violates row-level security policy for table "posts" DETAIL: Failing row contains (1, 'Title', user-uuid, 2026-01-15). ```

    Cause: WITH CHECK expression returned false. Fix: Check auth.uid() is being set correctly:

    ```javascript const { data, error } = await supabase.auth.getUser(); console.log('Current UID:', data.user.id); ```

    Error 2: Missing FROM Clause in Policy

    ``` ERROR: missing FROM-clause entry for table "other_table" ```

    Cause: Policy references a table without explicit JOIN/EXISTS. Fix: Use EXISTS subquery:

    ```sql -- ❌ Wrong WHERE team_id = team_members.team_id

    -- ✅ Right WHERE EXISTS ( SELECT 1 FROM team_members WHERE team_id = posts.team_id AND user_id = auth.uid() ) ```

    Error 3: Anon Role Cannot Access

    ``` ERROR: permission denied for schema public ```

    Cause: Public data needs explicit policy for anon role. Fix: Create policy granting anon SELECT on public rows:

    ```sql CREATE POLICY "Anonymous can view public posts" ON public.posts FOR SELECT TO anon USING (is_public = true); ```

    ---

    Testing Policies

    In Supabase Dashboard

    Go to SQL Editor and execute as different roles:

    ```sql -- Test as user SET request.jwt.claims = '{"sub": "your-user-uuid"}'; SELECT * FROM posts;

    -- Test as anon RESET request.jwt.claims; SELECT * FROM posts; ```

    In Your App

    ```javascript // Test as authenticated user const { data, error } = await supabase .from('posts') .select('*');

    if (error) { console.error('RLS denied:', error.message); // Likely: "new row violates row-level security policy" }

    // Test as anon const anonClient = supabase.createClient(URL, ANON_KEY); const { data: anonData } = await anonClient .from('posts') .select('*'); ```

    ---

    Production Checklist

  • ✅ RLS enabled on all user-owned/sensitive tables
  • ✅ No table without policies allows authenticated access
  • ✅ All policies tested with real JWT tokens (via [Supabase Auth docs](https://supabase.com/docs/guides/auth))
  • ✅ Custom JWT claims documented and validated
  • ✅ Audit logs enabled for CRITICAL tables (verify in [official RLS guide](https://supabase.com/docs/guides/auth/row-level-security))
  • ✅ Read [Security best practices](/?guide=postgres-security) before deploying
  • ---

    Performance Notes

    RLS policies run per-row. Complex nested EXISTS subqueries can slow large queries. Optimize with:

    1. Indexes on join columns: ```sql CREATE INDEX idx_team_members_user ON team_members(user_id); ```

    2. Denormalize critical paths if RLS logic is too complex

    3. Monitor slow queries in your Postgres logs

    ---

    Learn More

  • [Supabase RLS Official Docs](https://supabase.com/docs/guides/auth/row-level-security) — definitive reference
  • [PostgreSQL RLS Syntax](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) — low-level details
  • [Supabase Auth Integration](/?guide=supabase-auth) — how JWT tokens flow into policies
  • ---

    What am I missing?

    RLS is powerful but easy to misconfigure silently. Drop a comment if you've hit edge cases like:

  • Policies that work locally but fail in production
  • Performance issues with RLS on large tables
  • Custom JWT claim strategies you've used
  • Interactions between RLS and Realtime subscriptions
  • I'll update this guide based on your experiences.

    🔥 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