Guides / Supabase Row Level Security for vibe-coded apps

last checked oct 2026

Supabase Row Level Security for vibe-coded apps

Lovable, Bolt, v0 and most AI app builders put Supabase behind your frontend and your key in the browser. Row Level Security is the only thing standing between that key and your data.

Why RLS is not optional

Your Supabase publishable (or legacy anon) key ships in your frontend, and Supabase says that is fine, as long as RLS is on. Anyone can take that key and call your database API directly, skipping your UI entirely. Requests with the publishable key run as the anon role when signed out and authenticated when signed in. Policies decide what those roles can do.

Grants vs policies

Postgres checks two things. Grants decide whether a role can run an operation on a table at all. Policies decide which rows. Supabase notes that on many projects new tables in public automatically grant select, insert, update and delete to anon and authenticated, and that adding policies does not remove those grants. Their recommended setup does both:

alter table public.reports enable row level security;
revoke all on table public.reports from anon, authenticated;
grant select, insert, update, delete on table public.reports to authenticated;

Once RLS is enabled, publishable-key clients get nothing until you add policies.

Audit what you have

Run in the Supabase SQL Editor:

-- 1. Which public tables have RLS off?
select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;

-- 2. What do the policies actually allow?
select tablename, policyname, roles, cmd, qual, with_check
from pg_policies
where schemaname = 'public'
order by tablename, cmd;

The owner-only pattern

Write one policy per operation. Postgres does not accept several operations in one for clause, and Supabase advises against for all because it hides intent.

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

create policy "Users can create their own profile."
on profiles for insert to authenticated
with check ( (select auth.uid()) = user_id );

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

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

Public-read tables

alter table public.announcements enable row level security;
revoke all on table public.announcements from anon, authenticated;
grant select on table public.announcements to anon, authenticated;

create policy "Anyone can read announcements"
on public.announcements for select
to anon, authenticated
using ( true );

using (true) is only safe when the data is meant to be public and there is no write grant.

Mistakes we see constantly

Prove it with tests

Supabase recommends a pgTAP test file per table that asserts allow and deny for each operation and role:

supabase test new profiles_rls.test
supabase test db

Use set local role anon / authenticated and set local request.jwt.claim.sub = '<user id>' to impersonate, and check denied writes with returning rather than lives_ok, which passes on zero rows. The official page has full example files.

next step

Want a second pair of eyes?

Grab the free Lockdown Pack (security rules for Cursor and Claude Code plus a pre-launch checklist), or have us check the whole app for a fixed $99.

Sources

Every product step above was checked against these official pages. Last checked Oct 2026. Products change; if a menu moved, the docs below are the source of truth.