Home / RLS / v0 + Supabase security checklist
Supabase security · v0
v0 + Supabase security checklist
v0 apps are Next.js: anything in a NEXT_PUBLIC_ variable ships to the browser, and client components query Supabase with your anon key, so RLS still guards the data. Check for a leaked service_role key, public tables, always-true policies and anon-callable functions, then test signed out.
01 Why v0 apps need this
v0 generates Next.js. Server components and route handlers run on the server, but client components, and any code that uses a NEXT_PUBLIC_ variable, run in the visitor's browser. Supabase calls from the browser use your anon key, so Row Level Security decides what they can read and write.
Connecting Supabase through the Vercel integration adds environment variables to your project, including a server-side secret. The names vary between setups, so check them in your project settings: the URL and the anon / publishable key can be public, the service_role / sb_secret_ key must never get a NEXT_PUBLIC_ prefix. Supabase is retiring the legacy anon and service_role keys by the end of 2026 in favour of publishable and secret keys; the rule doesn't change.
02 Is the master key in your frontend?
The service_role key (or a new sb_secret_ key) bypasses every RLS policy. If it ships to the browser, anyone who opens your site can copy it and read or change your whole database. Builders put it there when it lands in a NEXT_PUBLIC_ variable or gets pasted into client code.
Scan your live app for it (free, paste the URL). Found one? Rotate it in the Supabase dashboard (Settings → API), move it to a server-only variable, redeploy and scan again.
.env
# fine in the browser: the URL and the anon / publishable key NEXT_PUBLIC_SUPABASE_URL=https://xyz.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=... # or a sb_publishable_ key # never with the NEXT_PUBLIC_ prefix: it would ship to every visitor SUPABASE_SERVICE_ROLE_KEY=...
03 Which tables can anyone read?
Your anon key is public by design; RLS is the only thing between it and your data. Run this in the Supabase SQL editor:
check.sql
select c.relname, has_table_privilege('anon', c.oid, 'select') as anon_can_read, c.relrowsecurity as rls_on from pg_class c join pg_namespace n on n.oid = c.relnamespace where n.nspname = 'public' and c.relkind = 'r' order by 1;
Any row with anon_can_read = true and rls_on = false is public: close it.
04 RLS is on, but is it doing anything?
"RLS enabled" isn't the same as "data protected". A policy that is always true lets everyone in, and generated code often contains one. List them:
check.sql
select tablename, policyname, cmd, roles, qual, with_check from pg_policies where schemaname = 'public' and (qual = 'true' or with_check = 'true') order by 1, 2;
Keep a row only if that table is public on purpose (a catalog, a status page). Otherwise replace it with a policy scoped to the user. The RLS policy generator writes one per operation, tested.
05 Functions anyone can call
A security definer function runs as its owner and skips RLS. In the public schema it's also an API endpoint (/rest/v1/rpc/…). List the ones a signed-out visitor can execute:
check.sql
select p.proname from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public' and p.prosecdef and has_function_privilege('anon', p.oid, 'execute') order by 1;
For each one: is it meant to be public? If not, revoke execute on function public.name(...) from public, anon; or move it to a schema the API doesn't expose.
06 Prove it as a stranger
The dashboard and the SQL editor bypass RLS, so they always look fine. Test the way an attacker would: open your app in a private window, signed out, copy the anon key from the network tab, and request a private table:
terminal
curl "$SUPABASE_URL/rest/v1/orders?select=*" -H "apikey: $ANON_KEY" # rows from other users → the policy is broken; [] → good
Then keep it fixed: npx airlock-rls in CI fails the build when a table ships without RLS or with an always-true policy, whoever (or whatever) wrote the migration.
Catch this before it ships
airlock-rls is a CI gate that fails your build when a table ships exposed or a policy is permissive: the same class of bug, caught in CI against a database with your migrations applied, before it reaches prod.
Or start from nextjs-supabase-starter : auth + a table with RLS + an isolation test, so you start from a pattern that's already sealed.
Grab the Supabase RLS cheat sheet
The golden rules, the footguns that leak in prod, correct policy snippets, and the isolation test — on one page.
FAQ
If I only query Supabase from server components, do I still need RLS?
Yes. Your anon key is still public (it's in the client bundle for auth), so anyone can call the API directly, bypassing your server code. RLS protects the data no matter which path the request takes.
Where should the service_role key be used?
Only in server code (route handlers, server actions, server components) and never imported into a client component. Prefer the user's session for normal queries so RLS applies; reserve the secret key for admin tasks.