Guide

Supabase Row Level Security: what AI app builders get wrong

Why Row Level Security is the real boundary in Supabase apps, five mistakes AI-built projects make with it, how to test your database, and policies that fix it.

VibeTect Resources4 min read

Key takeaways

  • 01In a Supabase app, the anon key is public by design. Row Level Security policies are what actually protect your data.
  • 02Tables without RLS, policies that allow everything, and service-role keys in the frontend are the most common failures.
  • 03You can test your exposure in minutes using only your app’s public key.
  • 04Policies created in the dashboard don’t live in your code — check them where they’re configured.

Supabase is the database behind a large share of apps built with AI app builders, and for good reason: it lets a frontend talk to Postgres directly through an auto-generated API. That convenience comes with one rule you can’t skip. The key in your frontend is public, so the database itself has to decide who can read and write each row. That’s what Row Level Security (RLS) does.

When RLS is missing or misconfigured, anyone who opens your site can extract the key and query your tables directly — no hacking skills required.

A real-world example

In 2025, a vulnerability was recorded as CVE-2025-48757, describing insufficient Row Level Security policies in apps generated by the Lovable platform that allowed remote, unauthenticated attackers to read or write arbitrary database tables of generated sites. Lovable disputed the CVE, stating that each customer is responsible for protecting their own application’s data.

Whatever your view on that dispute, the lesson for builders is the same: the platform generated the code, but the data exposure belongs to whoever shipped the app.

How Supabase security actually works

  • Your frontend holds the project URL and the anon (publishable) key. Both are visible to anyone.
  • Requests made with that key run as the anon role, or as authenticated once a user signs in.
  • RLS policies on each table decide which rows those roles can select, insert, update or delete.
  • The service-role key bypasses RLS completely. It belongs on a server, never in a browser or mobile app.

If you remember one thing: the anon key is not a secret, and it was never meant to be. RLS is the lock.

Five mistakes to look for

1. RLS isn’t enabled on a table

Tables created through SQL migrations — the way many AI tools create them — don’t automatically have RLS enabled. A table in an exposed schema without RLS can be queried with the anon key.

2. RLS is enabled, but a policy allows everything

An assistant asked to “fix the permission error” may write a policy like this:

create policy "Allow all" on public.profiles
  for all using (true) with check (true);

RLS is technically on. It just doesn’t restrict anything.

3. Reads are protected, writes aren’t (or the reverse)

Policies apply per operation. A table with a careful select policy but a permissive update or insert policy lets users modify data they can’t see — including other users’ rows or their own role.

4. The service-role key is in the frontend

When RLS blocks a query the app needs, the fastest “fix” is to use the service-role key. If that key is in a client component or a NEXT_PUBLIC_ / VITE_ variable, every user effectively has admin access to the database. See how AI-built apps leak API keys.

5. Views and functions bypass RLS

In Postgres, views run with the privileges of their owner by default, which can bypass RLS on the underlying tables. Functions declared security definer do the same. On Postgres 15 and later, create views with security_invoker = true so the caller’s permissions apply, and review every security definer function.

How to test your own database

Use the Security Advisor. The Supabase dashboard includes a Security Advisor that flags common issues, including tables without RLS. Start there.

List tables without RLS. In the SQL editor:

select schemaname, tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;

Any row where rowsecurity is false deserves attention.

Query as an attacker would. Using only your public URL and anon key — the values visible in your frontend — try reading each table through the REST API:

curl "https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*" \
  -H "apikey: YOUR_ANON_KEY"

If that returns other users’ data, so can anyone else’s request. Only run this against projects you own.

Policies that actually protect data

Enable RLS, then write explicit policies for each operation, tied to the signed-in user:

alter table public.profiles enable row level security;

create policy "Users can read their own profile"
  on public.profiles for select
  to authenticated
  using ((select auth.uid()) = id);

create policy "Users can update their own profile"
  on public.profiles for update
  to authenticated
  using ((select auth.uid()) = id)
  with check ((select auth.uid()) = id);

A few principles keep policies safe as your app grows:

  • No policy means no access. With RLS enabled, operations without a matching policy are denied. Add only what each role needs.
  • Use with check on writes, so users can’t insert or update rows into a state they wouldn’t be allowed to read.
  • Protect sensitive columns separately. If users can update their own row, make sure they can’t change columns like role or plan — move those to a separate table or restrict updates with a trigger or column privileges.
  • Keep policies in migrations. Policies created by clicking in the dashboard are invisible to code review and to any tool that inspects your repository.

Where local inspection fits

Because RLS lives in the database, it’s split across two places: your migration files, which are in your project, and the dashboard, which isn’t. A local inspection with VibeTect covers what’s in your project folder — for example, a service-role key used in client code or a secret placed in a public environment variable — so you can catch those problems before they’re deployed. For policies that exist only in the dashboard, use the Security Advisor and the test above.

Both checks together take less time than writing a single feature. Add them to your pre-deploy checklist.

Check it before you ship it.

Run a local security inspection on the project you’re about to deploy — and fix what it finds while it’s still cheap to fix.

Local analysis · No repository access required