How to Add Row Level Security to Supabase (2026)

About 60% of AI-built Supabase apps we scan have at least one table with Row Level Security disabled. Usually it's the main data table (the one that holds every user's records). Anyone with your Supabase URL and anon key can read all of it.

Fixing that isn't hard, but the order of operations matters. Enable RLS the wrong way and you lock out every user instantly. This guide walks through doing it right.

TL;DR

Write your policies before you enable RLS. Enable and activate in a single transaction. Roughly 60% of scanned AI-built Supabase apps have at least one unprotected table. You can fix yours in under 20 minutes.

Step 1: Find Every Table Without RLS

Run this in the Supabase SQL Editor. It lists every table in your public schema that doesn't have RLS enabled:

Audit: tables missing RLS
SELECT
  schemaname,
  tablename,
  rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
  AND rowsecurity = false
ORDER BY tablename;

You want this query to return zero rows. If it returns your users, posts, orders, or any other core table, those are unprotected right now.

Don't enable RLS yet

Run this audit first. Note every table name. You'll write policies for all of them before you flip the switch on any of them.

Step 2: Write Policies First

This is the step most guides skip. RLS starts denying all access the moment you enable it. If you don't have policies ready, your users hit empty tables or 403 errors.

Write policies with RLS still off. They sit dormant until you activate them.

The most common pattern for AI-built apps is user-scoped access: each user can only touch their own rows.

Core user-scoped policies
-- SELECT: users can only read their own rows
CREATE POLICY "users_select_own"
ON your_table
FOR SELECT
USING (auth.uid() = user_id);

-- INSERT: users can only add rows for themselves
CREATE POLICY "users_insert_own"
ON your_table
FOR INSERT
WITH CHECK (auth.uid() = user_id);

-- UPDATE: users can only change their own rows
CREATE POLICY "users_update_own"
ON your_table
FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);

-- DELETE: users can only delete their own rows
CREATE POLICY "users_delete_own"
ON your_table
FOR DELETE
USING (auth.uid() = user_id);

Replace your_table with your actual table name and user_id with whatever column stores the owner's user ID.

Check your column name

Not every table uses user_id. Bolt sometimes generates owner_id, Lovable might use created_by, and Cursor often follows whatever you described in the prompt. Run SELECT column_name FROM information_schema.columns WHERE table_name = 'your_table' to see what you've got.

Step 3: Enable RLS Atomically

Once policies exist, enable RLS in the same transaction. This is the key move: if anything fails partway through, the whole operation rolls back and nothing breaks.

Enable RLS atomically
BEGIN;

-- Enable RLS
ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;

-- Force RLS to apply even to the table owner
ALTER TABLE your_table FORCE ROW LEVEL SECURITY;

-- Verify policies are there before committing
SELECT policyname, cmd FROM pg_policies WHERE tablename = 'your_table';

COMMIT;

The FORCE ROW LEVEL SECURITY line is important if you connect to Supabase using the postgres superuser (some local dev setups do). Without it, the table owner bypasses all your policies.

If you have multiple tables to protect, run this pattern for each one separately, so a problem with one table doesn't cascade.

Step 4: Verify the Policies Work

Don't just check that your data appears. Check that someone else's data doesn't.

Test in SQL Editor:

Test as a specific user
-- Simulate a specific authenticated user
SET LOCAL request.jwt.claims TO '{"sub": "your-user-uuid", "role": "authenticated"}';

-- Should return only that user's rows
SELECT * FROM your_table;

-- Should return nothing
SELECT * FROM your_table WHERE user_id = 'some-other-user-uuid';

Test in your app:

  1. Log in as User A
  2. Note the ID of a row owned by User B (get it from the Supabase Table Editor)
  3. Try to fetch that row directly via your app
  4. You should get an empty result or 403, not the data

If you're using the Supabase client library, you can also check this in the Supabase Dashboard under Authentication > RLS Policies and use the built-in policy tester.

Step 5: Handle Edge Cases

A few patterns come up in almost every AI-built app:

Public tables (readable by anyone, like a products catalog):

Public read policy
CREATE POLICY "public_read"
ON products
FOR SELECT
USING (true);

ALTER TABLE products ENABLE ROW LEVEL SECURITY;

The profiles table (users need to read their own profile, and sometimes others' public info):

Profiles table policies
-- Anyone can read public profile fields
CREATE POLICY "profiles_public_read"
ON profiles
FOR SELECT
USING (true);

-- Users can only update their own profile
CREATE POLICY "profiles_update_own"
ON profiles
FOR UPDATE
USING (auth.uid() = id)
WITH CHECK (auth.uid() = id);

ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;

Storage buckets (people forget these have separate policies):

In the Supabase Dashboard, go to Storage > Policies. A "private" bucket still needs explicit policies on storage.objects to work correctly. Without them, even authenticated users get 403s on uploads and downloads.

Service role bypass

Your server-side code (Edge Functions, API routes, backend services) should use the service_role key, not the anon key. The service_role key bypasses all RLS. Keep it server-only; never expose it to the browser.

What the Scanner Sees

When CheckYourVibe scans your Supabase app, the most common finding is a table with RLS disabled and the anon key accessible from the client. We see this on roughly 60% of scanned AI-built Supabase projects.

The specific finding looks like this:

Supabase RLS Disabled

Table your_table has Row Level Security disabled. Any request using the anon key can read, insert, update, or delete all rows in this table without authentication checks.

Severity: High Fix: Enable RLS and add user-scoped policies.

This isn't hypothetical exposure. It's a live endpoint. The Supabase REST API for your project is publicly accessible, and the anon key is in your frontend bundle.

What happens if I enable RLS without adding policies first?

Every row becomes inaccessible to your users immediately. Supabase's default behavior with RLS enabled and no policies is to deny all access. Your users will see empty tables or 403 errors until you add policies.

Does enabling RLS affect my existing data?

No, enabling RLS doesn't delete or modify any data. It only changes who can see or edit rows. Your data stays intact; you're just adding access rules on top.

Can I enable RLS on one table without affecting others?

Yes. RLS is per-table. You can enable it on your users table today, leave orders unprotected for now, and add it to orders tomorrow. The change is isolated to the table you modify.

How do I let admins bypass RLS?

Use a service_role key in your server-side code. The service_role key bypasses all RLS policies by design. Keep it server-only: backend API routes, Edge Functions, and server components only.

Does storage have RLS too?

Yes. Supabase Storage buckets have their own security policies, separate from table RLS. Making a bucket private isn't enough; you also need policies on the storage.objects table to control who can upload and download.

How-To Guides

How to Add Row Level Security to Supabase (2026)