Supabase RLS checklist for vibe-coded apps
Supabase exposes your tables through an API that the browser calls with the public anon key. Row-level security (RLS) is what stops one user from reading or editing everyone else's data. AI builders often create tables without it, or with policies that allow everything.
The checklist
- RLS is enabled on every table in the
publicschema. Check Database → Tables, or the Security Advisor in the dashboard, or run:select tablename, rowsecurity from pg_tables where schemaname = 'public';
- No policy uses
using (true)for writes, andusing (true)on reads only for data that's truly public. - Owner-only access uses
auth.uid():alter table public.notes enable row level security; create policy "owners read" on public.notes for select to authenticated using ((select auth.uid()) = user_id); create policy "owners write" on public.notes for insert to authenticated with check ((select auth.uid()) = user_id); create policy "owners update" on public.notes for update to authenticated using ((select auth.uid()) = user_id) with check ((select auth.uid()) = user_id); create policy "owners delete" on public.notes for delete to authenticated using ((select auth.uid()) = user_id);
- UPDATE policies have a
with check, so users can't changeuser_idto someone else's. - Users can't make themselves admin. Don't keep roles or plan flags in a table users can update; store them in a separate table only the server can write, or in
app_metadata(notuser_metadata, which users can edit). - Storage buckets have policies too. Private files belong in non-public buckets with policies on
storage.objects, for example limiting access to a folder named afterauth.uid(). - Views and functions don't bypass RLS. Views run with their owner's rights by default; use
security_invoker = true(Postgres 15+). Be careful withsecurity definerfunctions. - The
service_rolekey is only on the server, never in frontend code. It bypasses RLS entirely. - Columns used in policies are indexed (e.g.
user_id) so policies stay fast.
Test it like an attacker
Use your public URL and anon key (both are in your site's JS) to query a table without signing in:
curl "https://YOUR-PROJECT.supabase.co/rest/v1/notes?select=*" \ -H "apikey: YOUR_ANON_KEY"
You should get an empty list or an error, not real rows. Then sign in as user A and try to read and update user B's rows; both should fail.
Prompt for your AI builder
Enable row level security on every table in the public schema. Add policies so users can only select, insert, update and delete rows where user_id = auth.uid(), with a WITH CHECK on insert and update. Never use USING (true) for writes. Move any role or plan columns to a table users cannot update.
Want a second pair of eyes?
LaunchLatch checks your app for this and the other common vibe-coding mistakes, with a report in 24 hours. The first 5 beta audits are free.
Get a free audit →Last updated October 16, 2026.