Home / Blog / Tutorial
Tutorial·7 min read·May 24, 2026

Supabase RLS for Vibe Coders: The 5-Minute Crash Course

If your Supabase tables have RLS enabled but every form submission returns new row violates row-level security policy, this is the post for you. RLS confuses everyone the first time. Here's how to think about it in plain English, plus the exact policies that cover 90% of what a vibe-coded app actually needs.

What RLS actually is

Row Level Security is Postgres's way of letting you write rules like "user A can see their own rows, user B can see theirs, and anonymous visitors can see nothing." It's enforced inside the database itself, which means your client-side code cannot bypass it — even if someone steals your publishable key, they can only do what your RLS policies allow.

This is why Supabase can safely give you a public API key in the browser: the safety net is RLS, not the key.

The mental model

Every Supabase table has two things you need to think about: whether RLS is on or off, and a list of policies. When a query hits the table:

  1. If RLS is off, every query goes through. (Dangerous default.)
  2. If RLS is on:
    • All queries are denied by default
    • Each policy adds permission for a specific action (SELECT, INSERT, UPDATE, DELETE)
    • At least one policy must say "yes" for the action to be allowed

So enabling RLS without writing policies = total lockdown. That's the error you're staring at right now. The table has the seatbelt on, but you forgot to give yourself a key.

The 4 policies that cover 90% of vibe-coded apps

1. Anyone can submit a form (contact, signup, newsletter)

sql create policy "anyone_can_insert" on public.contact_submissions for insert to public with check (true);

The to public part is what most tutorials get wrong. It works for anon, authenticated, AND any new role names Supabase introduces. Future-proof.

2. Users can read only their own rows

sql create policy "users_read_own" on public.invoices for select to authenticated using (auth.uid() = user_id);

The auth.uid() function returns the currently signed-in user's ID. As long as every row has a user_id column matching the owner, this is bulletproof multi-tenant isolation.

3. Users can update only their own rows

sql create policy "users_update_own" on public.invoices for update to authenticated using (auth.uid() = user_id);

Same shape. The using clause is the gate — it has to be true for both the row being read AND the row being updated.

4. Only specific admin emails can read everything

sql create policy "admin_read_all" on public.leads for select to authenticated using (auth.jwt() ->> 'email' in ('you@yourdomain.dev'));

This is how we lock the Mendly admin dashboard down. The JWT carries the signed-in user's email, and we check it against a whitelist. Anyone else gets an empty result set — not an error — which is intentional, it leaks nothing about whether the row exists.

The mistakes we see every single week

Mistake 1: Forgetting to public. If you write to anon and Supabase rolls out a new key system (they did, in 2025), your form breaks silently. Always use to public for anonymous inserts.

Mistake 2: Not enabling RLS at all. Supabase will warn you in the dashboard with a red icon. Ignoring it means your data is fully readable by anyone with the publishable key. Fix with alter table your_table enable row level security.

Mistake 3: Enabling RLS and forgetting policies. This is the 42501 error. Solution: add at least one policy per action you need. The most common version of this bug: you created tables via the Supabase UI, which enables RLS by default, but you never went back to add policies.

Mistake 4: Trusting the user's input for user_id. Never let the client send user_id in the body of a request. Always pull it from auth.uid() inside the policy. Otherwise a malicious user can write rows owned by anyone they want.

Mistake 5: Allowing UPDATE without restricting WHERE. A policy with using (true) on UPDATE means any authenticated user can change any row in the table. Always restrict to auth.uid() = user_id or similar.

How to test your RLS in 30 seconds

After writing any policy, run this cURL with your publishable key:

bash curl -X GET 'https://YOUR_PROJECT.supabase.co/rest/v1/your_table' \ -H 'apikey: YOUR_PUBLISHABLE_KEY' \ -H 'Authorization: Bearer YOUR_PUBLISHABLE_KEY'

  • Returns [] → RLS is working, anonymous can't read. ✓
  • Returns rows → your policy is too permissive. ⚠
  • Returns 401 → RLS is on but no SELECT policy exists. ⚠

When to call in a human

If your RLS rules are starting to feel like a maze — more than 6 policies on a single table, or weird "sometimes the user can read and sometimes they can't" bugs — that's a signal your data model itself is wrong. Usually it means you're trying to encode business logic into RLS instead of into a proper relationship structure. That's exactly the kind of thing we untangle on a Refactor Sprint.

TL;DR

  • RLS enabled + zero policies = locked down (this is the most common bug)
  • Always use to public for anonymous inserts
  • Always check auth.uid() for user-owned rows
  • Always check auth.jwt() ->> 'email' for admin-gated reads
  • Test with cURL after every change

That's all the RLS theory most vibe-coded apps will ever need. The rest is just applying these 4 patterns.

Vibe-coded an app that's breaking at scale? We'll audit it free in 48 hours.