A user fills in a form, presses Save, and nothing happens. The console or logs show "new row violates row-level security policy for table", followed by the table name. A public example: new row violates row-level security policy for table "players_secure" (supabase/postgres #1708).
Row-level security (RLS) is a set of rules on each table about who may read or write which rows. Supabase says that once RLS is enabled, "no data is accessible through the API when using a publishable key, until you create policies." PostgreSQL says inserted rows that do not pass the policy "will result in a policy violation error, and the entire INSERT command will be aborted."
So the database is protecting the table as designed. The fix is a policy that lets the right person write the right row, not switching RLS off. You can add policies in the Supabase SQL editor; replace the table and column names with yours.
1. There is no INSERT policy on the table
A table may have a policy for reading but none for adding rows.
Fix: add an insert policy for signed-in users that checks the row belongs to them, adapted from Supabase's profiles example:
create policy "Users can create their own rows"
on public.your_table for insert to authenticated
with check ( (select auth.uid()) = user_id );
Then save again from the app while signed in.
Source: Supabase: Row Level Security
2. The row does not carry the signed-in user's id
The policy compares a column (such as user_id) with the signed-in user. If the app leaves that column empty, or fills it with another id, the check fails.
Fix: in the code that saves, set user_id to the id of the signed-in user from the current session, and make sure the column name in the policy matches the column the app fills.
Source: Supabase: Row Level Security
3. The person is not signed in when the save runs
Supabase says that when a request has no authenticated user, auth.uid() returns null, so a policy like auth.uid() = user_id silently fails for them. It recommends auth.uid() IS NOT NULL AND auth.uid() = user_id. This happens when a form is used before sign-in, or when the session has expired.
Fix:
- Require sign-in before the screen that saves, or wait until the session exists.
- If the form is meant for visitors who are not signed in (a contact form, for example), write a separate insert policy for the
anonrole that allows only that table and only the fields it needs.
Source: Supabase: Row Level Security
4. The app asks for the new row back, and there is no SELECT policy
PostgreSQL says that if an insert returns the new row, "any newly inserted or updated rows from the relation must satisfy the relation's SELECT policies", and otherwise an error is thrown. In supabase-js this is an insert followed by .select().
Fix: add a select policy so people can read their own rows, or stop asking for the row back if the app does not need it.
create policy "Users can view their own rows"
on public.your_table for select to authenticated
using ( (select auth.uid()) = user_id );
Sources: PostgreSQL: CREATE POLICY · Supabase: Row Level Security
5. It is an upsert or an update
An upsert needs more than an insert policy. PostgreSQL checks the SELECT policies for an insert with ON CONFLICT DO UPDATE, and the UPDATE policy applies to its update part. Supabase also warns that without a SELECT policy, an UPDATE "will not work as expected."
Fix: add an update policy for the owner of the row, with both using and with check comparing (select auth.uid()) with user_id, and make sure a select policy exists too (cause 4).
Sources: PostgreSQL: CREATE POLICY · Supabase: Row Level Security
6. It is a file upload to Supabase Storage
Uploads are rows in the storage.objects table. Supabase says "by default Storage does not allow any uploads to buckets without RLS policies", and that the only policy needed for uploading is INSERT on storage.objects. Overwriting with upsert also needs SELECT and UPDATE.
Fix: add an insert policy on storage.objects for the authenticated role, limited to your bucket (Supabase's example checks bucket_id and the folder with storage.foldername(name)). If the app overwrites files, add select and update policies as well.
Source: Supabase Storage: Access control
Why you can't see it yourself
You're signed in, your customers aren't. You test as the owner, often from the builder, signed in, with rows that already exist. A brand-new account, or a visitor who is not signed in, is governed by the rules you never tried.
FAQ
Can I just turn row-level security off? The error disappears because the table becomes open to anyone with your public key, which is the opposite of what you want. Supabase also says never to use the secret or service_role key in the browser to get around it, because it bypasses RLS. Write the missing policy instead.
The same insert works in the SQL editor but fails from the app. Why? A public report (supabase/postgres #1708) describes exactly that. The app sends requests as the anon or authenticated role, and policies are written for those roles, so test policies as the role your app uses.
Fixed it? Prove a real stranger gets in
After the fix, the proof is a brand-new account saving its first row on your live app, and not seeing anyone else's. Your own account cannot show that, because it is not new. Your AI reads your code. VibeFix signs up like a stranger.
Free check. Paste your live app's link at vibe-fixer.com: an AI agent signs up to your live app once as a brand-new person, with a real inbox of its own, on a phone and on a computer. You get the problem count, what each problem is, and one screenshot. It never changes your app and never emails your users.
$9 for one app. Shows which cause is breaking your app and where, a fix prompt written for your app for every problem, plus 3 re-checks so you can see whether the fix holds for a stranger. It does not promise a fix; it shows what the agent saw.
Related: Users can see other users' data · Supabase "permission denied for table" (42501) · "Database error saving new user" on sign-up
VibeFix is independent. Supabase is a trademark of its owner.
Have you tried saving a row from a brand-new account on your live app, not your own?
Originally published at vibe-fixer.com. I'm building VibeFix, an AI agent that signs up to your live app like a brand-new person and shows what breaks.
Top comments (0)