The anon key is public, the service_role key isn't: an RLS checklist for AI-built Supabase apps A developer published an RLS checklist for Supabase apps generated by AI builders such as Lovable, Bolt, Cursor and v0, warning that tables created via raw SQL often ship without Row Level Security enabled. The checklist includes SQL queries to find tables with RLS off and overly permissive policies, plus a curl test against the REST API using only the public anon key to see what a stranger can read. The author notes that RLS-blocked reads return HTTP 200 with an empty array rather than a 401, so tests should target tables that contain data. AI builders like Lovable, Bolt, Cursor and v0 can wire a Supabase backend into your app in minutes. That's great, but the same few misunderstandings keep showing up in the apps they generate. None of them are exotic. They're the kind of thing that's easy to miss when the app "just works". Here's the checklist I run through on a Supabase app before calling it done. The publishable key sb publishable ... , or the legacy anon key is designed to live in your frontend. Anyone who opens dev tools can see it, and that's expected. What actually protects your data is Row Level Security RLS . Find tables in the public schema that don't have RLS turned on: select tablename, rowsecurity from pg tables where schemaname = 'public' order by rowsecurity, tablename; Anything with rowsecurity = false can be read, changed and deleted by anyone holding the public key, as long as the table's grants allow it. On older projects they do by default: every new table in public is granted to anon and authenticated automatically. Newer projects and new tables in existing projects from 30 Oct 2026 no longer get those automatic grants, but when an AI tool hits a permission denied error, its fix is often grant all on ... to anon , which puts you right back. See who can reach what: select table name, grantee, string agg privilege type, ', ' as privileges from information schema.role table grants where table schema = 'public' and grantee in 'anon', 'authenticated' group by table name, grantee order by table name; The Security Advisor in the Supabase dashboard flags tables with RLS off too. Tables you create in the Table Editor get RLS switched on by default, but tables created with raw SQL which is what AI tools often generate don't. Turn it on explicitly: alter table public.todos enable row level security; Careful on a live app: RLS with no policies blocks everyone using the public key, including your logged-in users. Reads come back empty and writes fail until you add policies. Add the policies from section 2 at the same time or test on a branch or staging project first . Server code using the secret key isn't affected. Once RLS is on, read every policy: select tablename, policyname, cmd, roles, qual, with check from pg policies where schemaname = 'public' order by tablename; Red flags: qual showing true on a SELECT policy that's using true means everyone can read every row. roles showing {anon} or {public} on a table that holds user data. to ... clause, and it includes logged-out visitors. with check showing INSERT or UPDATE policy, or an user id to auth.uid , so a user can write rows in someone else's name. A typical "users only see their own rows" setup looks like this it assumes a user id uuid column holding the owner's auth user id : create policy "Read own todos" on public.todos for select to authenticated using select auth.uid = user id ; create policy "Insert own todos" on public.todos for insert to authenticated with check select auth.uid = user id ; The dashboard runs queries as a privileged role, so it won't show you what a stranger sees. Ask the REST API directly with only the anon key: curl "https://