FixedLaunchBack

fix guide

Supabase RLS errors in AI-built apps, decoded

·

All fix guides

Apps from Lovable, Bolt, v0 and Cursor often keep their data in Supabase. Supabase protects each table with Row Level Security (RLS): rules, called policies, that decide which rows each visitor can read and change.

When a policy is missing or wrong, the app rarely says "RLS". You see an empty list, a save that fails, or an edit that disappears on refresh. Find your symptom below. Each step names the error text, what it means and the fix. The SQL runs in the SQL Editor in your Supabase dashboard. Replace the table and column names with yours.

1. The list is empty and there is no error

Supabase's docs say that once RLS is enabled, no data is accessible through the API with a publishable key until you create policies. A read with no matching policy doesn't fail. It returns zero rows.

So the table has rows in the dashboard, and the app shows nothing. Add a select policy. This one lets each signed-in user read their own rows:

create policy "Users can read their own rows" on todos for select to authenticated using ((select auth.uid()) = user_id);

If the page should be public, such as a list of published posts, write the policy for the anon role and limit it to the rows that are meant to be public.

The same silent pattern hits saves. Supabase data not saving, no error covers that side.

signs The rows are in the Table Editor and the app shows an empty list, with no red error in the browser console.

2. new row violates row-level security policy

This one does show an error. The insert has no policy that allows it, or the new row fails the policy's with check rule, often because user_id isn't sent.

It has its own guide with five causes and the SQL for each: New row violates row-level security policy in Supabase.

signs A save or upload fails and the error names the table.

3. An edit or delete does nothing

You change a record, the app says saved, and the old value is back after a refresh. Or a delete button runs and the row stays.

Supabase doesn't raise an error when a policy filters out every row an update or delete targets. Zero rows change and the request succeeds.

Each action needs its own policy. An update also needs a select policy: Supabase's docs say that without a SELECT policy, the UPDATE operation will not work as expected. Add both:

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

signs The request succeeds in the network tab, and the row in the Table Editor never changes.

4. infinite recursion detected in policy for relation

This error, code 42P17, shows up in apps with teams, workspaces or shared projects. A policy on a table such as members checks membership by reading the members table. Reading it runs the same policy again, and Postgres stops the loop.

Supabase's docs say to break the cycle with a security definer function. The function reads the table as its owner, so the policy doesn't run again inside it. Put it in a schema the API doesn't expose:

create schema if not exists private;
create function private.is_team_member(team uuid) returns boolean
language sql security definer set search_path = ''
as $$ select exists (select 1 from public.members where team_id = team and user_id = (select auth.uid())); $$;
create policy "Members can read their team's rows" on members for select to authenticated using (private.is_team_member(team_id));

Delete the old policy that read the members table directly, then test as a member and as a non-member.

signs Every request that touches the table fails with infinite recursion detected in policy for relation, followed by the table name.

5. permission denied for table

Code 42501 with permission denied means the role has no grant on the table. Supabase's docs say a missing grant raises 42501 before any policy is checked, so a perfect policy still fails.

Grant the actions your app uses to the role that needs them, usually authenticated. Step 5 of New row violates row-level security policy in Supabase has the line.

signs The error says permission denied for table and names it, and the policies look right.

6. It works in the browser and fails in server code

Server code, edge functions and API routes make their own Supabase client. If that client doesn't carry the signed-in user's token, the database sees a signed-out visitor, and every policy written for authenticated filters the rows away.

In Next.js, build the server client with the cookie helpers from @supabase/ssr. In an edge function, pass the Authorization header from the request into the client. Then auth.uid() returns the right user again.

Supabase's docs say a secret key is for administrative tasks that need to bypass RLS. Keep it in server code only. Never use it to make a user-facing page work.

signs The same query returns rows in the browser and nothing, or an RLS error, from a server page or function.

7. Two fixes that make it worse

Turning RLS off makes the error go away. Your publishable key sits in the site's code, and Supabase's docs say anyone can read it and it only reaches what Row Level Security allows. With RLS off on a table, any visitor with that key can read and change it.

Putting the service_role or secret key in the browser also makes the error go away. Supabase's docs say a secret key bypasses every Row Level Security policy and should never go in a browser, a shipped application or source control. If one leaked, delete it in the dashboard and make a new one.

If an AI builder offered either fix, undo it before launch. Is my vibe-coded app secure? Seven checks before launch has the other checks.

signs Your SQL history has alter table ... disable row level security, or your frontend code contains service_role.

Test it like a new user

  1. Sign in as user A, create a row, edit it and delete it. Check the Table Editor after each one.
  2. Sign in as user B in a private window and check you can't see or change user A's rows.
  3. Sign out and check private tables return nothing.
  4. Refresh a server-rendered page while signed in and check the data still shows.

Common questions

Why does my Supabase query return an empty array with no error?

RLS is on and the table has no select policy that matches the visitor. With RLS enabled, no data reaches the API through the publishable key until a policy allows it, and a read with no matching policy returns zero rows. Add a select policy for the right role.

How do I fix infinite recursion detected in policy for relation?

A policy on the table reads the same table, so the policy runs itself again. Move the membership check into a security definer function in a private schema, point the policy at that function, and delete the old policy that queried the table directly.

Is it safe to turn off RLS to fix the error?

No. Your publishable key sits in the site's code, and it only reaches what Row Level Security allows. With RLS off on a table, any visitor with that key can read and change it. Write the missing policy instead, and keep the secret key out of the browser.

Still stuck?

Not sure this is the cause? Paste your app's link into the free scan. It lists what a visitor's browser can see, like leaked keys and exposed files, worst first. The score is free; every fix as a prompt for your builder is $9, one-time.

Want a person to look inside? See the diagnosis

More fix guides

See all fix guides