Supabase Row Level Security: Beginner's Guide 2026

Master Supabase RLS with production patterns, real error messages, and exact PostgreSQL syntax. Secure your database at the row level.

TL;DR

Supabase Row Level Security (RLS) enforces database-level access control using PostgreSQL policies. Enable RLS on tables, define policies with CREATE POLICY, and authenticate users via supabase.auth.getUser(). Common mistakes: forgetting ENABLE ROW LEVEL SECURITY, using auth.uid() without proper user context, or missing USING/WITH CHECK clauses. Test policies with supabase-js v2.39.0+ (verify in official docs for latest).

---

What is Row Level Security?

Row Level Security is PostgreSQL's native feature that restricts database access at the row level—before data leaves the database. Instead of handling authorization in your application code, RLS lets you define policies that automatically filter queries.

Why it matters:

  • Prevents data leaks from application logic bugs
  • Works across all database clients (API, direct SQL, etc.)
  • Reduces boilerplate auth code
  • Integrates directly with Supabase Auth
  • Enabling RLS on a Table

    First, enable RLS on your table:

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

    Critical: Once RLS is enabled, *all* queries are blocked by default until you create policies. You'll see:

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

    Solution: Create at least one SELECT policy before testing.

    ---

    Creating Your First Policies

    Policy 1: Users Can Read Their Own Posts

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

    Breakdown:

  • FOR SELECT: Policy applies to read operations
  • USING: The filter condition. If false, row is excluded from results
  • auth.uid(): Built-in function returning authenticated user's UUID
  • user_id: Column in your posts table storing the post creator's ID
  • Policy 2: Users Can Insert Their Own Posts

    ```sql CREATE POLICY "Users can create posts" ON public.posts FOR INSERT WITH CHECK (auth.uid() = user_id); ```

    Key difference:

  • WITH CHECK: Validates data *before* insertion. If condition fails:
  • ``` Error: new row violates row-level security policy for table "posts" ```

    Policy 3: Users Can Update/Delete Only Their Posts

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

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

    Both clauses needed: USING checks current row ownership. WITH CHECK ensures updated/new row stays owned by user.

    Policy 4: Public Read Access

    ```sql CREATE POLICY "Posts are publicly readable" ON public.posts FOR SELECT USING (true); ```

    Use true to allow all authenticated users (or anon if you trust them). Order matters—PostgreSQL evaluates policies with OR logic by default.

    ---

    Production-Ready Implementation Pattern

    ```sql -- Users table (built-in from Supabase Auth) -- auth.users has auth_id UUID as primary key

    CREATE TABLE public.posts ( id BIGSERIAL PRIMARY KEY, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, title TEXT NOT NULL, content TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() );

    ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;

    -- Explicit policies for each operation CREATE POLICY "Authenticated users can read published posts" ON public.posts FOR SELECT TO authenticated USING ( -- User sees own posts or posts marked public auth.uid() = user_id OR published = true );

    CREATE POLICY "Users insert with their own ID" ON public.posts FOR INSERT TO authenticated WITH CHECK (auth.uid() = user_id);

    CREATE POLICY "Users update own posts" ON public.posts FOR UPDATE TO authenticated USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);

    CREATE POLICY "Users delete own posts" ON public.posts FOR DELETE TO authenticated USING (auth.uid() = user_id); ```

    Pattern notes:

  • TO authenticated: Restricts to logged-in users. Omit for anon access
  • Multiple OR conditions: User sees posts they own OR posts published publicly
  • ON DELETE CASCADE: When user deletes account, their posts auto-delete
  • ---

    Common Errors & Solutions

    Error 1: "Permission Denied for Table"

    ``` Error: permission denied for table posts ```

    Cause: RLS enabled but no SELECT policy exists, or user doesn't match any policy.

    Fix: Create a SELECT policy: ```sql CREATE POLICY "temp_all_read" ON posts FOR SELECT USING (true); ```

    Error 2: "auth.uid() is Null"

    ``` Error: permission denied (called in SELECT FROM posts where auth.uid() returned NULL) ```

    Cause: User isn't authenticated. You're accessing from anonymous client or auth session expired.

    Fix: Verify auth before querying: ```typescript const { data: { user } } = await supabase.auth.getUser(); if (!user) throw new Error('Not authenticated'); ```

    Error 3: "New Row Violates RLS Policy"

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

    Cause: INSERT/UPDATE data doesn't satisfy WITH CHECK clause. Common: trying to set user_id to different user's ID.

    Fix: Never trust client input for user_id: ```typescript const { data, error } = await supabase .from('posts') .insert([ { title: 'My post', content: 'Content here', user_id: user.id // Always from auth context, never request body } ]); ```

    ---

    Testing RLS Locally

    Use Supabase's [SQL Editor](https://supabase.com/docs/guides/database/overview) to test policies:

    ```sql -- Test as specific user SET request.jwt.claims = json_build_object('sub', '550e8400-e29b-41d4-a716-446655440000');

    SELECT * FROM posts; ```

    Better approach: Use [supabase-js](https://supabase.com/docs/reference/javascript/introduction) (v2.39.0+) to test with real auth:

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

    if (error) console.error('RLS blocked:', error.message); else console.log('Posts visible:', data); ```

    ---

    Key Takeaways

    1. Enable RLS: ALTER TABLE table_name ENABLE ROW LEVEL SECURITY 2. Write policies: Use CREATE POLICY with USING for SELECT/DELETE, WITH CHECK for INSERT/UPDATE 3. Never trust clients: Set user context server-side, never from request body 4. Test thoroughly: Verify policies work before deploying to production 5. Reference docs: See [Supabase RLS Guide](https://supabase.com/docs/guides/auth/row-level-security) and [PostgreSQL Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html)

    ---

    Related Guides

  • [Building Secure Multi-Tenant Apps](/?guide=multi-tenant-rls)
  • [Supabase Auth Setup for Teams](/?guide=supabase-auth-teams)
  • ---

    What am I missing?

    Have you encountered RLS edge cases? Comment with:

  • Policies you needed for complex authorization (teams, hierarchies, temporal access)
  • Performance gotchas with deeply nested OR conditions
  • Testing strategies that worked for your team
  • Version-specific behavior changes you've discovered
  • Accuracy matters to this community—corrections welcome.

    🔥 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