DEV Community

Not Elon
Not Elon

Posted on

Your Supabase RLS Is Probably Wrong: A Security Guide for Vibe Coders

You built your app with Lovable, Cursor, or Bolt. You connected Supabase. You enabled Row Level Security because the docs said to.

Your RLS is probably wrong.

I have scanned dozens of vibe-coded apps this month. The same RLS mistake appears in roughly 80% of them. The app works perfectly. Every feature functions. Users can sign up, create data, view their data. And every user can also view every other user's data.

The mistake

Here is what AI-generated RLS policies typically look like:

CREATE POLICY "Users can view data"
ON public.user_data
FOR SELECT
USING (auth.role() = 'authenticated');
Enter fullscreen mode Exit fullscreen mode

This policy says: if you are logged in, you can read all rows. Every row. Every user's data.

Here is what it should say:

CREATE POLICY "Users can view their own data"
ON public.user_data
FOR SELECT
USING (auth.uid() = user_id);
Enter fullscreen mode Exit fullscreen mode

The difference is one function call. auth.role() checks if someone is logged in. auth.uid() checks if the logged-in user owns that specific row. One character of difference in the code, total difference in security.

Why AI tools get this wrong

When you prompt Lovable or Cursor to "add authentication to my app," the AI does exactly what you asked. It adds login and signup. It creates the database tables. It enables RLS.

But the AI optimizes for "works correctly" not "works securely." The policy it generates passes every test: authenticated users can read data, unauthenticated users cannot. That is technically correct. It is also a data breach.

Nobody prompts the AI to add ownership checks. The AI does not add them unprompted. The result is an app that looks secure but leaks data between users.

How to check yours in 2 minutes

Open the Supabase SQL editor and run:

SELECT
  schemaname,
  tablename,
  policyname,
  qual
FROM pg_policies
WHERE schemaname = 'public'
ORDER BY tablename;
Enter fullscreen mode Exit fullscreen mode

Look at the qual column. If you see (auth.role() = 'authenticated'::text) on any table that stores user data, you have the problem.

If you see (auth.uid() = user_id) or something similar with auth.uid(), you are probably fine for that table.

The fix

For each table that stores user-specific data:

-- Drop the broken policy
DROP POLICY "Users can view data" ON public.user_data;

-- Create the correct one
CREATE POLICY "Users can view their own data"
ON public.user_data
FOR SELECT
USING (auth.uid() = user_id);

-- Do the same for INSERT, UPDATE, DELETE
CREATE POLICY "Users can insert their own data"
ON public.user_data
FOR INSERT
WITH CHECK (auth.uid() = user_id);

CREATE POLICY "Users can update their own data"
ON public.user_data
FOR UPDATE
USING (auth.uid() = user_id);

CREATE POLICY "Users can delete their own data"
ON public.user_data
FOR DELETE
USING (auth.uid() = user_id);
Enter fullscreen mode Exit fullscreen mode

Replace user_data with your actual table name. Replace user_id with whatever column stores the user's UUID in that table.

Common variations that are also broken

Service role bypass:

USING (auth.role() = 'service_role' OR auth.role() = 'authenticated')
Enter fullscreen mode Exit fullscreen mode

Still broken. Any authenticated user reads everything.

True for all:

USING (true)
Enter fullscreen mode Exit fullscreen mode

No restriction at all. Anon users can read everything. This appears more often than you would expect.

Missing policies entirely:
Some tables have RLS enabled but zero policies. This actually blocks all access (Supabase defaults to deny), which is better than the wrong policy. But it usually means the developer disabled RLS on that table to "fix" their app when queries stopped working.

Tables that need ownership checks

Not every table needs auth.uid() = user_id. Here is a quick breakdown:

Needs ownership check: profiles, user_data, orders, messages, documents, settings, anything with personal or financial data

Does not need ownership check: public content (blog posts set to published), lookup tables (countries, categories), app configuration

Needs role-based check: admin panels, moderation queues, shared team data

What happens when this is wrong

A user creates an account on your app. They open browser DevTools. They go to the Network tab. They find a Supabase request. They copy the URL and anon key (both visible in the JavaScript bundle). They run:

curl 'https://your-project.supabase.co/rest/v1/user_data?select=*' \
  -H "apikey: your-anon-key" \
  -H "Authorization: Bearer their-jwt-token"
Enter fullscreen mode Exit fullscreen mode

Every user's data comes back. Names, emails, financial data, health data, whatever you store. One curl command.

Beyond RLS

RLS is the most common issue but not the only one. Other things to check:

  1. API keys in your JS bundle. Open your deployed site, view source, search for eyJ. Your Supabase anon key will be there (expected). Anything else should not be.

  2. Auth email verification. Is it required? If not, anyone can sign up with any email and start querying.

  3. Rate limiting. Are your auth endpoints rate-limited? If not, brute force attacks work.

  4. Storage bucket policies. If you use Supabase Storage, the bucket policies have the same RLS problem. Check them separately.

We built free security tools specifically for this at notelon.ai. The code scanner checks for exposed keys and common misconfigurations. No signup required.

If you want a deeper review covering all of the above plus business logic, auth flows, and API design, our $99 security audit covers 50+ checks with a PDF report.


Part of the Vibe Coding Security series. More guides on Lovable, Cursor, Bolt, and Windsurf security at notelon.ai/report.

Top comments (0)