Supabase RLS disabled: anyone with your anon key can read your whole table
Your Supabase anon key is in your client bundle. That's by design — it's supposed to be public. The thing that stops a stranger from select *-ing your users table with it is Row Level Security. If RLS is off, there is nothing between your database and the open internet.
AI coding tools do this constantly. Cursor, Lovable and Bolt will happily scaffold a table and skip the RLS policy, because the app works fine without it in development. It works fine for attackers too.
How to tell if you're exposed
Option 1 — Supabase dashboard (30 seconds)
Open the Supabase dashboard → Table Editor. Any table without the green “RLS enabled” badge is public to anyone with your anon key.
Option 2 — SQL query (check all tables at once)
Paste this into the Supabase SQL Editor:
SELECT
tablename,
rowsecurity AS rls_enabled
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY rls_enabled, tablename;Any row with rls_enabled = false is exposed.
Option 3 — Test it the way an attacker would (60 seconds)
Grab your anon key from Settings → API in your Supabase dashboard (or open DevTools on your deployed site and search the JS bundle for eyJ). Then:
curl "https://YOUR_PROJECT.supabase.co/rest/v1/users?select=*" \
-H "apikey: YOUR_ANON_KEY" \
-H "Authorization: Bearer YOUR_ANON_KEY"If you get rows back: you're exposed. If you get:
{"code":"42501","details":null,"hint":null,"message":"permission denied for table users"}RLS is working correctly.
users with the actual name of your table. Try profiles, orders, subscriptions — every table that holds user data needs checking separately.Why AI tools create this pattern
In development, your Next.js app calls the Supabase client using the service_role key (via a server-side route) or uses authenticated sessions for reads. Either way, the app works. No error, no warning, no failing test.
AI coding tools complete the task they're given — “add a users table” or “set up Supabase auth” — and the task works. They're not running a security test suite against your production API. They're not an attacker.
The gap is that your app calls supabase.from('users').select() with the right context. An attacker calls the raw PostgREST API over HTTP with nothing but your anon key, which is sitting in plain text in your deployed JS bundle.
How to fix it (3 steps)
Step 1 — Enable RLS on the table
-- In a new Supabase migration or the SQL editor:
ALTER TABLE users ENABLE ROW LEVEL SECURITY;Step 2 — Create policies for each operation
The most common pattern — users can only read and write their own row:
-- Users can view their own profile
CREATE POLICY "users_select_own"
ON users
FOR SELECT
TO authenticated
USING (auth.uid() = id);
-- Users can update their own profile
CREATE POLICY "users_update_own"
ON users
FOR UPDATE
TO authenticated
USING (auth.uid() = id)
WITH CHECK (auth.uid() = id);
-- Users can delete their own account
CREATE POLICY "users_delete_own"
ON users
FOR DELETE
TO authenticated
USING (auth.uid() = id);If your app inserts a new row when a user signs up, add an INSERT policy too:
-- Users can only insert a row for themselves
CREATE POLICY "users_insert_own"
ON users
FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = id);For a table that should be genuinely public (e.g. a blog posts table):
-- Anyone can read posts (including unauthenticated visitors)
CREATE POLICY "posts_public_read"
ON posts
FOR SELECT
TO anon, authenticated
USING (true);
-- Only the author can write
CREATE POLICY "posts_author_write"
ON posts
FOR ALL
TO authenticated
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);Step 3 — Verify the fix
Re-run the SQL check from step 1 — all rows should now show rls_enabled = true:
SELECT tablename, rowsecurity AS rls_enabled
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;Then re-run the curl test. You should now get a permission denied error or an empty array:
curl "https://YOUR_PROJECT.supabase.co/rest/v1/users?select=*" \
-H "apikey: YOUR_ANON_KEY" \
-H "Authorization: Bearer YOUR_ANON_KEY"
# → {"code":"42501","message":"permission denied for table users"} ✓Common gotchas
RLS enabled + no policies = everything blocked
This is the correct and secure default, but it surprises developers. The moment you enable RLS, you must explicitly grant access via policies for every operation you want to allow. If your app suddenly returns empty arrays or permission errors after enabling RLS, this is why.
Views don't inherit RLS from their base tables
If you create a view over a table with RLS, the view does not automatically enforce the same policies. You need to either define security on the view itself, or use SECURITY INVOKER (PostgreSQL 15+) so the view runs with the calling user's permissions.
The service_role key bypasses RLS entirely
The service_role key is designed for administrative tasks that need unrestricted access. It skips all RLS policies. Never expose it in client-side code, never put it in NEXT_PUBLIC_ variables, and never commit it to your repository. Treat it like a database root password.
JOINs can leak data across RLS boundaries
If table A has RLS but table B doesn't, a query that JOINs A to B can expose B's rows to users who have access to A. Audit every table that any user-facing query touches.
auth.uid() returns null for unauthenticated requests
If your policy uses auth.uid() = user_id and an unauthenticated request comes in, auth.uid() is null and the USING clause evaluates to false — which is what you want. But if you accidentally use = ANY instead of = and your column allows null, you may get unexpected results.
How vibecheck detects this
vibecheck clones your repository and walks every .sql file under supabase/, migrations/, db/, and database/ directories. It extracts every table name created by a CREATE TABLE statement and checks whether a matching ALTER TABLE … ENABLE ROW LEVEL SECURITY exists for it.
Tables without a matching RLS statement get a CRITICAL finding with the exact table name. This check runs before any code is deployed, so you catch the gap before it reaches production.
FREE SCAN
Check your repo for missing RLS in 60 seconds
Connect GitHub → vibecheck scans your SQL migrations, API routes, secrets, and 40+ other checks. Claude explains every finding in plain English.
scan my repo for free →FAQ
My app works fine without RLS — why does it matter?+
Your app works because it calls the right endpoints in the right order. An attacker skips your app entirely and calls the PostgREST API directly with your anon key, which is already in your JS bundle. Without RLS, that gives them SELECT * on every table in your public schema.
Will enabling RLS break my existing app?+
Enabling RLS without creating policies blocks all access for all users — including authenticated ones. Your app will return empty results or permission errors until you add the policies it needs. Test in a staging environment first, and add policies for every table operation (SELECT, INSERT, UPDATE, DELETE) your app performs.
What about tables that are supposed to be public?+
Enable RLS on them anyway, then create a policy with USING (true) to allow unrestricted reads. This makes the intent explicit and auditable: "this table is intentionally public" rather than "we forgot to add RLS". It also means a future developer won't turn RLS on and accidentally block everything.
Does the service_role key bypass RLS?+
Yes — completely. The service_role key is designed for administrative operations that need unrestricted database access. It bypasses all RLS policies. Never expose it in client-side code, never put it in a NEXT_PUBLIC_ environment variable, and never commit it to your repository.
How does vibecheck detect missing RLS without running my app?+
vibecheck performs a shallow clone of your repository and scans the SQL files in your migrations or supabase directory. It finds every CREATE TABLE statement and checks whether a matching ALTER TABLE … ENABLE ROW LEVEL SECURITY appears anywhere in your SQL files. This is a static check — no database connection needed.
Does this check work if I use Supabase's dashboard to create tables instead of migrations?+
Not for those tables — vibecheck can only read files in your repository. If you created tables through the Supabase dashboard without generating migration files, run the SQL check in Step 1 manually to audit your live schema.
Related vulnerabilities
RLS protects the database layer. These checks protect the layers above it: