Supabase RLS Explained: The Security Hole in Most AI-Built Apps
If you built your app with Lovable, Bolt or v0, there's a good chance it runs on Supabase. And there's a real chance that anyone on the internet can read your users' data right now. Not because Supabase is insecure. Because of one setting that's easy to get wrong: Row Level Security (RLS) . This po

If you built your app with Lovable, Bolt or v0, there's a good chance it runs on Supabase. And there's a real chance that anyone on the internet can read your users' data right now.
Not because Supabase is insecure. Because of one setting that's easy to get wrong: Row Level Security (RLS).
This post explains what RLS is, the mistakes that leave data exposed, and how to check your own app in five minutes.
Supabase gives every project an API that talks directly to your Postgres database. Your frontend calls it with the anon key (newer projects call it the publishable key ).
So what stops someone from using your key to read every row in your profiles or orders table?
Only RLS. RLS policies are rules inside Postgres that decide which rows each request is allowed to see or change. If RLS is off, the public key is effectively a master key to that table.
Tables created in the Supabase dashboard's Table Editor have RLS enabled by default. Tables created with raw SQL or migrations, which is how many AI tools generate them, often don't.
With RLS off, this request returns every row:
curl 'https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*' \ -H "apikey: YOUR_ANON_KEY" \ -H "Authorization: Bearer YOUR_ANON_KEY" Enter fullscreen mode Exit fullscreen mode 2. RLS is on, but the policy allows everything When you turn RLS on with no policies, Postgres blocks everything and the app "breaks." The quick fix people (and AI tools) reach for is:
create policy "allow all" on profiles for select using ( true ); Enter fullscreen mode Exit fullscreen mode using (true) means "every row, for everyone." RLS is technically on, but it protects nothing.
A common gap: a solid select policy, but an insert or update policy without with check. A user can then write rows with someone else's user_id, or update a field like role = 'admin'.
The service role key (newer projects: secret key ) skips RLS completely. It belongs only on a server. If you can find it in your frontend code or.env files that get bundled, rotate it now.
user_metadata ( raw_user_meta_data ) can be edited by the user. A policy like "allow if the user's metadata says role = admin " can be bypassed by the user simply editing their own metadata. Store roles in app_metadata or a separate table only your server can write.
Here's a typical "users only see their own rows" setup for a notes table:
-- 1. Turn RLS on alter table public. notes enable row level security; -- 2. Read: only your own notes create policy "Users can read their own notes" on public. notes for select to authenticated using ( ( select auth. uid ()) = user_id ); -- 3.
Create: only notes that belong to you create policy "Users can create their own notes" on public. notes for insert to authenticated with check ( ( select auth. uid ()) = user_id ); -- 4. Update: only your notes, and you can't hand them to someone else create policy "Users can update their own notes" on public. notes for update to authenticated using ( ( select auth. uid ()) = user_id ) with check ( ( select auth. uid ()) = user_id ); -- 5. Delete: only your own notes create policy "Users can delete their own notes" on public. notes for delete to authenticated using ( ( select auth. uid ()) = user_id ); -- 6.
Keep it fast create index on public. notes ( user_id ); Enter fullscreen mode Exit fullscreen mode Two details worth knowing:
Step 1: find tables with RLS off. Run this in the Supabase SQL Editor:
select tablename, rowsecurity from pg_tables where schemaname = 'public' order by rowsecurity, tablename; Enter fullscreen mode Exit fullscreen mode Any table with rowsecurity = false is readable with your public key.
select tablename, policyname, cmd, qual, with_check from pg_policies where schemaname = 'public'; Enter fullscreen mode Exit fullscreen mode Look for qual or with_check equal to true, and for insert / update policies with an empty with_check.
Step 3: open the Security Advisor. In the Supabase dashboard, the Security Advisor flags tables without RLS and other common issues. Fix everything marked as an error.
Step 4: test as a stranger. Run the curl request from mistake #1 against your important tables. If you get real data back without logging in, that table is exposed.
None of this is specific to AI tools. I worked with the same idea for years with Firebase security rules: the browser can never be trusted, so every permission has to be enforced in the database or on the server. AI builders are great at getting the screens working. They're less reliable at this invisible layer, because a missing policy doesn't show up as a bug until someone finds it.
If your app handles real user data, it's worth an hour to check before launch.
I'm Muhammad Moiz, a full-stack developer who helps founders take AI-built apps to production. If you'd like a second pair of eyes on your Supabase setup, I offer a fixed-price production audit.