Supabase and RLS policies: what AI forgets and how to check it
In AI-built apps on Supabase, data security depends on RLS policies and on how you handle roles: the most common mistakes and how to check for them.
Author
G. Tempesta
Published
Reading
5 min
Updated
Written with AI assistance, reviewed by Giovanni Tempesta
Almost all the apps I review, built with Lovable, Bolt or Cursor, use Supabase as their database. It is a good choice, with a consequence many people are unaware of: the security of your data depends almost entirely on the database’s RLS policies. And they are exactly the part AI forgets, or writes too loosely.
Why security in a Supabase app depends on RLS policies
In a Supabase app the browser talks directly to the database, through the APIs generated by Supabase and the public key (anon, or the new publishable key). It really is public: anyone who opens the site can see it. That is fine, because what decides what can be read and written is not the key, but Postgres Row Level Security (RLS).
How it works, in short:
- every request reaches the database with a Postgres role:
anonif the user has not signed in,authenticatedif they have. Theservice_rolerole, on the other hand, bypasses RLS; - on each table you enable RLS and write the policies: one condition for each command (
SELECT,INSERT,UPDATE,DELETE) and for each role; - the
USINGclause decides which rows can be read, updated or deleted; theWITH CHECKclause decides which rows can be written; - inside the policy you use
auth.uid(), the signed-in user, for exampleuser_id = auth.uid().
If RLS is disabled, the table has no protection; if the policies are wrong, the protection is wrong. With RLS enabled and no policies, on the other hand, nothing gets through.
The RLS policy mistakes I find most often
1. Tables with RLS disabled
AI creates a new table for a new feature and forgets to enable RLS. That table is open to anyone with the public key.
2. “True for everyone” policies
The policies are there, but they say USING (true): everyone the policy applies to reads everything; if it applies to anon, so does anyone who has not signed in. It is usually a quick fix added when something was not working, and then left there.
3. Policies for reading, not for changing
Users only see their own data, good. But they can change other people’s, because the UPDATE and DELETE policies are missing or different. Or the WITH CHECK is looser than the USING, for example WITH CHECK (true): users update one of their own rows and assign it to someone else. The same goes for INSERT: with WITH CHECK (true) rows can be created in other people’s names.
4. Roles in the wrong place: RBAC done badly
Almost every app has roles (admin, operator, user) and plans (free, Pro): this is role-based access control, RBAC. The typical mistake is keeping the role or plan in a profile column that users can change: with a direct call they promote themselves.
RBAC done properly on Supabase:
- roles live in a separate table (for example
user_roles) that users can read but not write; - alternatively, the role travels in the access token as a custom claim, added by Supabase Auth with a Custom Access Token Hook (a removed role stays valid until the token is refreshed);
- never the role in the profile’s
user_metadata: it can also be read withauth.jwt(), but users can change it themselves. If you really need it, it goes inapp_metadata, which only the server writes; - policies read the role from there, with
auth.jwt()or with a check function, and do not trust what the browser sends; - the paid plan is written only by the server, when the payment confirmation arrives.
5. Views and functions that bypass RLS
In Postgres a view runs with the permissions of whoever created it, and therefore ignores the table’s policies, unless it is created with security_invoker = true. The same applies to SECURITY DEFINER functions: useful inside policies, dangerous if anyone can call them from the API. Supabase flags views that bypass RLS in the Security Advisor; for functions, check who can execute them.
6. Files
File buckets (Storage) have their own policies too. A public bucket holding private documents is one of the most common holes.
7. The service key in the wrong place
The service_role key (or one of the new secret keys) bypasses all policies. It can only live in code that runs on the server. If you find it in the frontend or in the repository, it is the first thing to fix.
How to check, without being an experienced developer
- Open the Supabase Security Advisor and read the warnings. They are clear.
- Check the list of tables: each one must have RLS enabled and at least one policy for every operation it needs.
- Run the two-user test: create two accounts, A and B. Sign in as B and try to see or change something belonging to A (an order, a document, a profile). If you can, you have found a problem.
- Try to change your own role: with a normal user, try to change the role or plan column. If you can, the RBAC needs redoing.
- Look at the file buckets: which ones are public, and what is inside them.
What these checks do not find
These checks find the biggest problems, not all of them. The policies of a real app can be many and intertwined, and the mistake is often in the detail: a missing condition, a forgotten role, a case nobody tested. Before opening the app to real users, or before adding payments and personal data, it is worth having them reread by someone who did not write them.
With the AI Code Check-up I check the RLS policies and roles of your Supabase app, along with everything else, and tell you what to fix first. The price is fixed. Before I see the code, we sign a confidentiality agreement together, and I read the code without changing it. The AI Code Check-up page