Supabase says no: RLS errors in AI-built apps
Somewhere between your app and its data, Supabase said no:
new row violates row-level security policy for table "projects"
Or worse, it said nothing: every list in the app is empty, every query
returns [], status 200, no error anywhere.
Both are row-level security (RLS) — and in AI-built apps this is not an edge case, it is the default state of a new table. The AI creates tables with RLS enabled (correct!) and then forgets some of the policies that let your app through (the bug). Nothing warns about a policy that does not exist.
The model in one paragraph
With RLS enabled, every query runs as somebody — a signed-in user
(authenticated) or the public key (anon) — and a policy is a rule
saying which rows that somebody may SELECT, INSERT, UPDATE or
DELETE. No policy for an operation means nobody may do it through your
app's keys. The anon key in the browser is expected to be public; RLS is
the security. Which is why the tempting fix — disable RLS — is not a fix:
it publishes the table to the internet.
Failure 1 — inserts refused
new row violates row-level security policy (HTTP 401/403, Postgres code
42501) on insert. Three causes, in order of frequency:
No INSERT policy at all. The table got RLS at creation and only a SELECT policy, or none. Add one:
create policy "Users insert their own rows"
on public.projects for insert to authenticated
with check (auth.uid() = user_id);
The policy exists but its check fails. with check (auth.uid() = user_id) requires the inserted row's user_id to be the caller — and the
generated code never sets user_id. Either set it in the insert, or default
it in the table (user_id uuid default auth.uid()).
There is no session. If the user is not actually signed in — token
expired, sign-up flow never completed — auth.uid() is null and every such
check fails. Log supabase.auth.getUser() before the insert to know which
world you are in.
A companion trap: Supabase returns the inserted row by default, which also needs a SELECT policy. With INSERT allowed and SELECT missing, the write succeeds and the code still errors reading the answer.
Failure 2 — reads come back empty
No error, no 401 — just [], everywhere, forever. For reads, RLS does not
refuse: it filters. A missing SELECT policy filters everything.
create policy "Users read their own rows"
on public.projects for select to authenticated
using (auth.uid() = user_id);
-- Or, for data that is genuinely public to read:
create policy "Anyone reads" on public.articles for select to anon, authenticated
using (true);
If the app then still shows nothing, the empty array is probably crashing optimistic rendering code — that error has its own guide.
Failure 3 — “Database error saving new user”
Sign-up itself fails with this message when a trigger copies new users into
a profiles table and that insert hits RLS or a constraint. The real
error is in the Supabase dashboard: Logs → Postgres. The usual fix is
making the trigger function security definer (it runs with the function
owner's rights, which is the point of a system trigger), or adding the
INSERT policy the trigger needs.
Hand it to the AI
Paste this to the AI that built your app
My app uses Supabase and hits RLS problems. The exact error is: [paste the message and status — or "queries return empty arrays with no error"] The failing operation is [insert/select/update] on table [name], performed as [a signed-in user / the anon key]. Show me the current policies for that table, then write the missing ones with the correct to-role and auth.uid()-based checks. Verify the code sets every column the policies check (like user_id). Do NOT disable RLS on any table, and never use the service-role key in frontend code.
Prove it
Test as the person the policies describe: a fresh account in a private window — sign up, create a row, read it back, then confirm a second fresh account cannot see the first one's row. That last check is the entire point of RLS, it takes one minute, and generated apps fail it more often than anything else on this page.
Questions people also ask
Can I just disable RLS to make the error go away?
That makes the error go away by making the protection go away: with RLS off and your anon key public (it always is), anyone on the internet can read and write that table directly. Write the missing policy instead — it is four lines of SQL.
Why do my SELECTs return an empty array instead of an error?
That is how RLS works for reads: rows you may not see are filtered out, not refused. A missing SELECT policy makes every query come back empty with status 200 — which looks exactly like 'no data yet' and is the most misleading failure in the family.
Why did inserting break when the app saves through a signed-in user?
Usually the policy checks auth.uid() = user_id but the code never sets user_id on the inserted row, or the request is actually running without a session (the token expired or sign-in never completed), so auth.uid() is null and every such check fails.