Supabase Row Level Security: Beginner Guide 2026

Master RLS policies in Supabase with production-ready patterns, real error messages, and step-by-step implementation for indie hackers.

TL;DR

Supabase Row Level Security (RLS) lets you enforce database-level access control automatically. Enable RLS on tables, create policies matching your auth logic, and test thoroughly. Common mistakes: forgetting auth.uid(), mixing up USING vs WITH CHECK, and not testing unauthenticated access.

---

What is Row Level Security?

Row Level Security is a PostgreSQL feature that restricts database query results based on policies you define. With Supabase, you get PostgreSQL + built-in auth, making RLS the best place to enforce data access rules.

Why not app-level checks? Because users can bypass your frontend. RLS lives at the database—impossible to circumvent.

Current stability: RLS in Supabase (as of 2026) is production-ready. [Official Supabase docs](https://supabase.com/docs/guides/auth/row-level-security) cover the latest implementation.

---

Step 1: Enable RLS on Your Table

First, enable RLS. Without this, policies don't apply:

```sql ALTER TABLE profiles ENABLE ROW LEVEL SECURITY; ```

Critical: Once enabled, *all* queries are blocked by default unless a policy explicitly allows them. Test immediately.

---

Step 2: Create Your First Policy

Let's say users should only read their own profile:

```sql CREATE POLICY "Users can read own profile" ON profiles FOR SELECT USING (auth.uid() = user_id); ```

Breakdown:

  • FOR SELECT — applies to read queries only
  • USING — the condition that must be true for a row to appear
  • auth.uid() — the authenticated user's UUID from Supabase Auth
  • user_id — your table's foreign key to auth.users
  • ---

    Policy Types: USING vs WITH CHECK

    Two clauses control different operations:

    USING — applies to SELECT and DELETE ```sql CREATE POLICY "Users can delete own posts" ON posts FOR DELETE USING (auth.uid() = author_id); ```

    WITH CHECK — applies to INSERT and UPDATE ```sql CREATE POLICY "Users can update own posts" ON posts FOR UPDATE USING (auth.uid() = author_id) -- can they access it? WITH CHECK (auth.uid() = author_id); -- can they write to it? ```

    Both required? On UPDATE, yes. USING filters which rows they can access; WITH CHECK ensures they can't escalate privileges by writing to author_id.

    ---

    Real Error Messages You'll See

    Error 1: "new row violates row-level security policy"

    ``` PostgresError: new row violates row level security policy "Users can update own posts" for table "posts" ``` Cause: Your WITH CHECK clause failed. Usually means you're trying to update a row you don't own, or change author_id to someone else's.

    Fix: Verify your policy allows the update, or check you're passing the correct user_id.

    Error 2: "permission denied for schema public"

    ``` PostgresError: permission denied for schema "public" ``` Cause: RLS is enabled, you're unauthenticated, and no policy allows public access.

    Fix: Either create a policy for unauthenticated users, or ensure your client sends a valid JWT token.

    Error 3: "relation does not exist"

    ``` PostgresError: relation "auth.users" does not exist ``` Cause: Typo in table reference, or assuming auth schema is accessible (it's not in some Supabase setups).

    Fix: Use auth.uid() directly; don't query auth.users in policies.

    ---

    Production-Ready Pattern: Role-Based Access

    Most apps need different permissions per role. Here's the pattern:

    ```sql -- Add role to profiles ALTER TABLE profiles ADD COLUMN role TEXT DEFAULT 'user';

    -- Admins can see all profiles CREATE POLICY "Admins read all profiles" ON profiles FOR SELECT USING ( (SELECT role FROM profiles WHERE id = auth.uid()) = 'admin' );

    -- Users see only themselves CREATE POLICY "Users read own profile" ON profiles FOR SELECT USING (auth.uid() = id); ```

    Warning: Querying the same table in USING can cause recursion. For complex roles, use a separate roles table:

    ```sql CREATE TABLE roles ( user_id UUID PRIMARY KEY REFERENCES auth.users(id), role TEXT NOT NULL );

    CREATE POLICY "Admins read all profiles" ON profiles FOR SELECT USING ( (SELECT role FROM roles WHERE user_id = auth.uid()) = 'admin' ); ```

    ---

    Testing RLS Policies

    Always test both authenticated and unauthenticated scenarios:

    ```sql -- Test as authenticated user SET request.jwt.claims = '{"sub": "550e8400-e29b-41d4-a716-446655440000"}'; SELECT * FROM profiles; -- Should show only that user's profile

    -- Test as unauthenticated RESET request.jwt.claims; SELECT * FROM profiles; -- Should return 0 rows (unless you allow it) ```

    In the Supabase dashboard, use the SQL editor with Impersonate User button to test as real users.

    ---

    Common Mistakes

    1. Forgetting to enable RLS first Policies exist but don't restrict anything. Always run ALTER TABLE ... ENABLE ROW LEVEL SECURITY;

    2. Using auth.users in policies You can't access auth.users directly in RLS. Use auth.uid(), auth.jwt(), or custom claims.

    3. Mixing up column names If your table uses user_uuid but Supabase sends auth.uid() (UUID), they must match exactly.

    4. Not testing unauthenticated access Once RLS is on, unauthenticated users see nothing unless you explicitly allow it. Test this.

    ---

    Debugging RLS Issues

    Enable query logging:

    ```sql ALTER SYSTEM SET log_statement = 'all'; SELECT pg_reload_conf(); ```

    Then check Postgres logs in Supabase dashboard (Settings → Logs).

    For client-side debugging, log the JWT: ```javascript const { data: { session } } = await supabase.auth.getSession(); console.log(session?.access_token); // Decode on jwt.io to inspect claims ```

    ---

    Next Steps

    Once you master basic RLS, explore:

  • [Custom JWT Claims for advanced authorization](/?guide=supabase-custom-claims)
  • [Real-time subscriptions with RLS](/?guide=supabase-realtime-rls)
  • For deeper PostgreSQL RLS details, see [official PostgreSQL docs](https://www.postgresql.org/docs/current/ddl-rowsecurity.html).

    ---

    What am I missing?

    RLS can get complex fast. Have you hit edge cases with:

  • Recursive table references?
  • Performance issues on large tables?
  • RLS with foreign key cascades?
  • Testing RLS in CI/CD pipelines?
  • Drop your questions and gotchas in the comments. Corrections welcome—accuracy matters here.

    🔥 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