Supabase RLS vibe coding.
Vibe Coding Rescue

Supabase RLS for Vibe-Coded Apps: The Policies Your AI Forgot to Write

8 min read | Updated September 2026

If your app was built by describing it to an AI and it uses Supabase, there is a good chance every logged-in user can read every other user’s data right now. Not because Supabase is insecure — because Row Level Security is off, or the policy the AI wrote says true. This is the single most common finding in our vibe-coded app reviews, and it takes about an hour to fix properly.

Check in 60 seconds

Open the Supabase dashboard → Table Editor. Any table showing “RLS disabled” or “Unrestricted” is readable and writable by anyone holding your public anon key — which is every visitor to your site, since the anon key ships in the browser. Then open Authentication → Policies and look for policies whose expression is just true. Those are the same thing with extra steps.

Why AI tools get this wrong

Supabase’s anon key is meant to be public. The security model assumes that RLS policies on every table decide who can see what. When you ask an AI to “make the app show my tasks”, the fastest path to a working demo is a table with RLS off or a permissive policy, because then nothing blocks the query. The demo works. The tool reports success. Nobody wrote the policy that says only the owner, and nothing in the UI reveals that it’s missing.

This is the concrete version of the authorisation gap in our vibe coding security checklist: the app checks that you’re logged in (Supabase Auth does that well) but never checks that the row belongs to you.

The four policies almost every app needs

Most vibe-coded apps have the same shape: users, and things that belong to users. For any table with a user_id column, the baseline is four policies — one per operation — all keyed on auth.uid():

alter table public.tasks enable row level security;

create policy "owner can read"   on public.tasks for select using (auth.uid() = user_id);
create policy "owner can insert" on public.tasks for insert with check (auth.uid() = user_id);
create policy "owner can update" on public.tasks for update using (auth.uid() = user_id) with check (auth.uid() = user_id);
create policy "owner can delete" on public.tasks for delete using (auth.uid() = user_id);

Three details the AI usually misses: insert needs with check (not using), otherwise anyone can insert rows attributed to someone else; update needs both, so a user can’t reassign their row to another account; and the user_id column should default to auth.uid() and be not null, so the client can’t omit it.

The checklist

1
RLS enabled on every table in the public schema
Not just the ones you remember. Junction tables, logs, settings, uploads metadata. A single unrestricted table with a user_id column leaks who your users are.
2
No policy evaluates to true for anon or authenticated
Legitimate exceptions exist (a public read-only plans table). Every one should be deliberate and written down.
3
Shared data goes through a membership table, not a wide policy
Teams, workspaces, shared projects: the policy checks that auth.uid() exists in a memberships row for that resource. Use a security definer helper function to avoid recursive policy errors.
4
The service-role key is not in the client
It bypasses RLS entirely. Search the deployed bundle for service_role. If it’s there, rotate it today and move whatever needed it to a server-side function.
5
Storage buckets have policies too
Uploads live in Storage with their own policy set. A public bucket for user documents is the same leak with a different URL.
6
Edge functions and RPCs check the caller
A database function marked security definer runs with elevated rights; it must validate auth.uid() itself. AI-generated RPCs frequently don’t.
7
You tested it as the wrong user
Two accounts, one browser each. Try to read, update and delete the other account’s rows through the app and through a direct REST call with the anon key. Both should fail.

Tell the AI exactly this

Once you know what “right” looks like, the tool is good at producing it. A prompt that works: “For every table in the public schema, enable RLS and write owner-only policies for select, insert, update and delete keyed on auth.uid() = user_id. Use with check on insert and update. List every table you changed and any table where an owner-only policy doesn’t make sense, and stop so I can decide those.” Then run check 7 yourself — the tool will report success either way.

If the schema doesn’t have a clean user_id on the tables that need one, or ownership is more complicated than “mine vs. not mine”, that’s a data-model conversation — the one check in our 12-point prototype audit that decides whether hardening is enough or the core needs re-spining.

FAQ

Is Supabase safe for a vibe-coded app?
Yes — once RLS is on and the policies are correct. Supabase’s model is designed for exactly this setup. The risk comes from shipping with the defaults an AI chose to make the demo work.
Will enabling RLS break my app?
Enabling RLS with no policies blocks everything, so the app will appear broken until the policies exist. Write the policies first, enable RLS, then test as two users. Do it on a staging project if you have real users.
Does Firebase have the same problem?
Yes, in the form of Firestore/Realtime Database security rules. The failure mode is identical: rules that allow everything so the demo works. The fix is the same shape.
I already have users. Is it too late?
No, but treat it as urgent and quiet: fix the policies, check logs for unusual access if you have them, and if you’re in a regulated space, get advice about disclosure obligations.

Want the policies reviewed before real users find the gap?

Fixed-scope Supabase security review for AI-built apps: every table, every policy, every bucket — with the two-user test done and documented.

Book a Supabase Review →

Engineering Insights

Latest from Syntaxa Studio.

Loading latest posts