← Back to Blog
SUPABASE March 22, 2026 7 min read

Supabase RLS: The One Setting That Exposes All Your User Data

If you are using Supabase and you have not explicitly enabled Row Level Security (RLS) on every table, your database is publicly readable. Not "maybe." Not "in theory." Right now, anyone can query your entire database using just your project URL and anon key -- both of which are in your client-side JavaScript.

This is the single most common critical vulnerability we find at vibeAudit. Over 60% of the Supabase apps we scan have at least one table with RLS disabled. Let us fix that.

What Is Row Level Security?

Row Level Security is a PostgreSQL feature that Supabase exposes through its dashboard. When RLS is enabled on a table, every query must pass through a policy that you define. If there is no matching policy, the query returns zero rows. If RLS is disabled, every query returns every row.

Think of it this way:

  • RLS OFF = the table is a public spreadsheet. Anyone with the URL can read everything.
  • RLS ON, no policies = the table is locked. Nobody can read anything (including your app).
  • RLS ON, with policies = the table is properly secured. Each user sees only what you allow.

Why This Is Critical

Your Supabase anon key is not a secret. It is embedded in your client-side JavaScript and visible to anyone who opens browser DevTools. Supabase designed it this way intentionally -- the anon key is supposed to be safe because RLS restricts what it can access.

But when RLS is disabled, the anon key becomes an unrestricted access pass. Here is what an attacker can do:

# An attacker only needs your project URL and anon key
# (both visible in your JavaScript bundle)

curl 'https://YOUR_PROJECT.supabase.co/rest/v1/users?select=*' \
  -H "apikey: YOUR_ANON_KEY" \
  -H "Authorization: Bearer YOUR_ANON_KEY"

# If RLS is disabled, this returns EVERY row in the users table:
# emails, names, profile data, private settings -- everything

This is not hypothetical. We see this in production apps every day.

How to Enable RLS

Step 1: Enable RLS on every table

Go to your Supabase SQL Editor and run:

-- Find all tables that don't have RLS enabled
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';

-- Enable RLS on each table
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.messages ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;
-- Repeat for EVERY table in your public schema

Step 2: Add read policies

After enabling RLS, your app will break because no one can read anything. You need to add policies that define who can access what.

-- Users can read their own profile
CREATE POLICY "Users can read own profile"
  ON public.profiles
  FOR SELECT
  USING (auth.uid() = id);

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

-- Public posts are readable by everyone (if you want that)
CREATE POLICY "Public posts are readable"
  ON public.posts
  FOR SELECT
  USING (is_published = true);

-- Users can only manage their own posts
CREATE POLICY "Users manage own posts"
  ON public.posts
  FOR ALL
  USING (auth.uid() = author_id);

Step 3: Add insert and delete policies

-- Users can only insert rows with their own user_id
CREATE POLICY "Users insert own data"
  ON public.messages
  FOR INSERT
  WITH CHECK (auth.uid() = sender_id);

-- Users can only delete their own messages
CREATE POLICY "Users delete own messages"
  ON public.messages
  FOR DELETE
  USING (auth.uid() = sender_id);

Common RLS Mistakes

Even after enabling RLS, there are several patterns that create false security:

Mistake 1: Overly permissive policy

-- BAD: This allows anyone to read everything (same as no RLS)
CREATE POLICY "Allow all" ON public.profiles
  FOR SELECT USING (true);

-- GOOD: Restrict to the authenticated user
CREATE POLICY "Read own data" ON public.profiles
  FOR SELECT USING (auth.uid() = user_id);

Mistake 2: Forgetting the service_role bypass

-- The service_role key BYPASSES RLS entirely.
-- NEVER use it in client-side code.
-- Only use it in server-side functions (Edge Functions, API routes).

-- If your Supabase client is initialized with the service_role key
-- in client code, RLS gives you zero protection.

Mistake 3: Not covering all operations

-- If you only add a SELECT policy, users can still INSERT,
-- UPDATE, and DELETE without restriction (if RLS is enabled
-- but only SELECT has a policy, other operations are denied
-- by default -- which is safe, but might break your app).

-- Best practice: explicitly define policies for each operation
-- SELECT, INSERT, UPDATE, DELETE

How to Test If Your RLS Is Working

After setting up your policies, test them manually:

# Test with the anon key (unauthenticated)
curl 'https://YOUR_PROJECT.supabase.co/rest/v1/profiles?select=*' \
  -H "apikey: YOUR_ANON_KEY" \
  -H "Authorization: Bearer YOUR_ANON_KEY"

# Expected: empty array [] or error (not a list of all users)

# Test with a user's JWT token (authenticated as User A)
curl 'https://YOUR_PROJECT.supabase.co/rest/v1/profiles?select=*' \
  -H "apikey: YOUR_ANON_KEY" \
  -H "Authorization: Bearer USER_A_JWT_TOKEN"

# Expected: only User A's profile, not other users' data

If unauthenticated requests return user data, your RLS is not configured correctly.

Automate the Check

Manually testing RLS policies after every schema change is tedious and error-prone. vibeAudit automates this: our Supabase-specific scanner detects your project URL, checks every table for RLS status, and tests whether unauthenticated requests can access data they should not.

The free scan covers RLS detection. Run it every time you add a new table or change your policies.

Is your app vulnerable?

Scan your app for free and find out in minutes.

Scan Your App Free