Supabase Row Level Security: Beginner Guide 2026

Master Supabase RLS with practical examples, common errors, and production patterns. Learn to secure your PostgreSQL data at the row level.

TL;DR

Supabase Row Level Security (RLS) enforces data access rules at the PostgreSQL level. Enable RLS on tables, create policies with CREATE POLICY, and use auth.uid() to reference current users. Common mistakes: forgetting to enable RLS before creating policies, using incorrect claim paths, and not testing with authenticated vs. unauthenticated sessions.

---

What is Row Level Security?

Row Level Security (RLS) is a PostgreSQL feature that restricts which rows users can access based on policies you define. Unlike application-level checks, RLS enforces rules at the database layer—no authenticated user can bypass it, even with direct API calls.

Supabase (verify current pricing in [official docs](https://supabase.com/docs/guides/auth/row-level-security)) exposes PostgreSQL's RLS through its client libraries and REST API. When you query via supabase-js or the PostgREST API, RLS automatically filters results based on the authenticated user's session.

---

Enable RLS on Your Table

First, you must enable RLS. Without this, policies won't apply:

```sql -- In Supabase SQL Editor ALTER TABLE posts ENABLE ROW LEVEL SECURITY; ```

Critical: If you create policies before enabling RLS, they exist but don't enforce. Enable RLS first.

Verify status in SQL Editor: ```sql SELECT schemaname, tablename, rowsecurity FROM pg_tables WHERE tablename = 'posts'; ```

---

Basic Policy Pattern

Policies specify which rows a user can SELECT, INSERT, UPDATE, or DELETE. Here's the production pattern:

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

-- Users can only insert posts as themselves CREATE POLICY "Users can insert own posts" ON posts FOR INSERT WITH CHECK (auth.uid() = user_id);

-- Users can update only 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);

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

Breakdown:

  • FOR SELECT – read operations
  • USING() – condition for what rows are visible
  • FOR INSERT/UPDATE – write operations
  • WITH CHECK() – validates data being written
  • auth.uid() – built-in Supabase function returning current user's UUID
  • ---

    Real-World Error Messages

    Error 1: Policies Don't Apply Because RLS Isn't Enabled

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

    Or silently returning all rows. Fix: Run ALTER TABLE tablename ENABLE ROW LEVEL SECURITY;

    Error 2: Incorrect JWT Claim Path

    ``` PG::UndefinedFunction: ERROR: function auth.uid() does not exist ```

    Occurs if you're using a custom PostgreSQL user instead of Supabase's auth context. Fix: Ensure queries run through Supabase client with valid JWT token, or use current_user_id() if defined in your schema.

    Error 3: Missing User ID in INSERT

    ``` PG::ExclusionViolation: ERROR: new row violates row-level security policy ```

    Happens when WITH CHECK (auth.uid() = user_id) fails because user_id is null or doesn't match. Fix: Always include the authenticated user's ID when inserting:

    ```javascript const { data, error } = await supabase .from('posts') .insert([{ title: 'My Post', user_id: (await supabase.auth.getUser()).data.user.id }]); ```

    ---

    Advanced Patterns

    Public + Private Data

    Some rows are public, others private:

    ```sql CREATE POLICY "Public posts visible to all" ON posts FOR SELECT USING (is_public = true OR auth.uid() = user_id); ```

    Anonymous users see only is_public = true. Authenticated users see their own posts too.

    Team-Based Access

    Users access rows belonging to their team:

    ```sql CREATE POLICY "Users access team documents" ON documents FOR SELECT USING ( team_id IN ( SELECT team_id FROM team_members WHERE user_id = auth.uid() ) ); ```

    Admin Bypass

    Admins bypass RLS (use carefully—verify in [official docs](https://supabase.com/docs/guides/auth/row-level-security#admin-bypass)):

    ```sql CREATE POLICY "Admins bypass RLS" ON posts FOR ALL USING ( auth.uid() IN (SELECT id FROM profiles WHERE is_admin = true) ); ```

    ---

    Testing RLS Policies

    Always test with authenticated and unauthenticated sessions:

    ```javascript // Test 1: Unauthenticated query const unauth = await supabase .from('posts') .select('*'); // Should return 0 rows or public rows only

    // Test 2: Authenticated as user A const { data: userA } = await supabase.auth.signInWithPassword({ email: 'a@example.com', password: 'password' });

    const { data: postsA } = await supabase .from('posts') .select('*'); // Should return only user A's posts

    // Test 3: Attempt unauthorized write const { error } = await supabase .from('posts') .update({ title: 'Hacked' }) .eq('id', 'user-b-post-id') .select(); // Should error: "new row violates row-level security policy" ```

    ---

    Common Pitfalls

    1. Forgetting WITH CHECK on writes – Always include it to validate inserted/updated data matches your policy.

    2. Using string IDs instead of UUIDsauth.uid() returns UUID. Ensure your user_id column is also UUID type.

    3. Testing only authenticated users – Verify unauthenticated access returns empty results or errors appropriately.

    4. Policies stacking unpredictably – If multiple policies match, PostgreSQL ORs them. Use FOR ALL sparingly.

    5. Not testing with real JWT tokens – Supabase CLI's supabase test command can help. Verify in [official docs](https://supabase.com/docs/guides/auth/row-level-security).

    ---

    Debugging Checklist

  • [ ] Is RLS enabled on the table? (ALTER TABLE ... ENABLE ROW LEVEL SECURITY)
  • [ ] Does the policy use correct column names and types?
  • [ ] Is auth.uid() returning the expected UUID?
  • [ ] Are you sending a valid JWT token in the request header?
  • [ ] Does the data satisfy WITH CHECK conditions?
  • [ ] Are you testing with the correct user account?
  • ---

    Next Steps

    For deeper patterns, explore [Supabase RLS Best Practices](https://supabase.com/docs/guides/auth/row-level-security) and learn about [custom claims](/?guide=supabase-custom-claims). For multi-tenant apps, check [Multi-Tenant Security](/?guide=supabase-multitenant).

    ---

    What am I missing?

    RLS is powerful but context-dependent. What patterns have you found essential? Have you hit unexpected errors or edge cases? Drop your experiences, corrections, and questions in the comments—this guide improves with your feedback.

    🔥 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