"Supabase security" for an AI-built app means two different things. The first is the project configuration: what Supabase exposes by design and what you have to close yourself. The second is what Lovable, Bolt or Cursor do with Supabase when they generate the code, which is where almost all the holes come from. This guide covers both, with three SQL queries to audit your project in a minute, a review prompt for your AI and the way to check from outside that everything is closed.
Part 1: how security works in Supabase
The data API is public by design
Supabase puts a REST API (PostgREST) in front of your Postgres that the browser calls directly with the anon key (or the newer publishable key). That key ships in the JavaScript of every user and that is normal: it identifies the project, it grants no permissions. What decides who can read or write each row is Row Level Security. The other key, service_role (or secret), bypasses RLS entirely and can only live on a server. If you understand this split, you understand 80% of Supabase security.
RLS and policies: the only real barrier
A new table in the public schema is reachable through the API the moment you create it. Without RLS enabled, anyone with the anon key reads and writes all its rows. With RLS enabled and no policies, nobody gets in. With policies, whoever they say. Hence the rule: RLS on every table and one explicit policy per operation (select, insert, update, delete). The concrete patterns (each user owns their rows, team membership, public read-only content) are in what to review in Supabase before you launch.
What bypasses RLS without you noticing
- Views. By default a view runs with the permissions of whoever created it, meaning it ignores the RLS of the tables it queries. Create them with
with (security_invoker = on). - Security definer functions. An RPC function marked that way runs as its owner and reads whatever it wants. Use them only when strictly needed, with
set search_path = '', and revoke execution fromanonif a visitor should not call it. - Exposed schemas. Everything in
public(tables, views and functions) is reachable through the API. If a function is internal, move it to another schema. - The service_role key. In the frontend, in a public repository or in a prompt pasted into an AI, it is full control of the database.
Auth: settings worth a look
Under Authentication you have email confirmation (leave it on), the list of allowed Redirect URLs (if it is empty or has wildcards, a magic link can land on someone else's domain), rate limits, captcha for sign-up and recovery, the password policy, leaked password protection and anonymous sign-ins (off unless you use them on purpose). Each is a checkbox; none is wrong by default, but the AI tends to flip them so that "the tests pass".
Storage, Realtime and Edge Functions
Public buckets serve any file to whoever has the URL: for user uploads use private buckets, policies on storage.objects and signed URLs with an expiry, plus a size limit and allowed types per bucket. In Realtime, Postgres changes respect RLS, but broadcast and presence channels need their own policies if they carry private data. In Edge Functions, secrets go through supabase secrets set, never in the code, and if you disable JWT verification for a webhook, validate the sender's signature yourself.
The project itself
The direct Postgres connection string (with the database password) must never leave your server or your environment variables. Enable SSL enforcement, consider network restrictions if you are on a paid plan, and check the dashboard's Security Advisor (Database, Advisors) from time to time: it flags tables without RLS, security definer views, functions with a mutable search_path and exposed auth tables.
Part 2: what AI gets wrong with Supabase
These are the mistakes that come up most often in apps generated with Lovable, Bolt or Cursor, in order of frequency:
- Creating tables without RLS. The generator writes the migration, the app works and nobody enables anything. It is the origin of Lovable's CVE-2025-48757, which exposed data from hundreds of projects.
using (true)policies. When RLS blocks something, the AI "fixes it" with a policy that lets everything through. It is RLS disabled under another name.- service_role in the browser. A query fails on permissions and the quick fix is to swap the key. Search for
service_rolein your bundle right now. - Security definer functions to dodge errors. Same pattern: instead of writing the right policy, the query gets wrapped in a function that runs as superuser.
- Public buckets for user files. Avatars, fine; invoices, documents or exports, no.
- Privilege fields the user can edit. A
roleoris_adminin theprofilestable with an update policy that does not exclude those columns. Anyone makes themselves admin from the browser console. - Disabling email confirmation or rate limits so tests run faster, then forgetting it in production.
Three SQL queries to audit your project
Paste them into the dashboard's SQL editor. The first lists public tables without RLS; ideally it returns nothing:
select n.nspname as schema, c.relname as table
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where c.relkind = 'r' and n.nspname = 'public' and not c.relrowsecurity;
The second finds policies that let everything through:
select tablename, policyname, cmd, roles
from pg_policies
where schemaname = 'public' and (qual = 'true' or with_check = 'true');
Such a policy on a public read-only content table can be correct; on profiles, orders or messages, it is not. The third lists functions that run with their owner's permissions:
select p.proname, pg_get_function_identity_arguments(p.oid) as args
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.prosecdef;
Every result should have a reason you can explain. If it does not, drop the security definer or move it out of public.
A review prompt before you deploy
Paste it into Lovable, Bolt or Cursor with access to your schema and your code:
Review the security of this Supabase app and do not change anything yet. Answer with a list:
1. Tables in the public schema without RLS enabled.
2. Policies with using (true) or with check (true), and whether they are justified.
3. Privilege columns (role, is_admin, plan, credits) a user could modify with the current update policies.
4. Views without security_invoker and security definer functions, with the reason for each.
5. Any use of the service_role key or the Postgres connection string outside the server.
6. Public buckets containing user files.
7. Auth settings that were disabled (email confirmation, rate limits, captcha) and redirect URLs with wildcards.
For each point, propose the exact SQL or change, and wait for my confirmation.
Check the result from the outside
The definitive test is not the dashboard, it is what the API answers to a stranger. With your project URL and the anon key (both are in the browser code, so an attacker has them too):
curl -s "https://YOUR_PROJECT.supabase.co/rest/v1/profiles?select=*" -H "apikey: YOUR_ANON_KEY" -H "Authorization: Bearer YOUR_ANON_KEY"
If it returns rows, that table is open. If it returns [] or a permissions error, RLS works. Repeat for every sensitive table and with a regular authenticated user to confirm they only see their own data. Scanaris runs this check for you: it detects the Supabase URL and key in your bundle, probes the usual tables without a session, looks for the service_role key and the connection string in the JavaScript, checks where your app stores the session token and gives you a fix prompt for every finding. If Lovable generated your app, see the Lovable security guide too; if the problem is keys in the client, secrets your app leaks in the browser.
Frequently asked questions
Is it a problem that the anon key is in the browser?
No. It is designed for that and only identifies the project. What protects the data is RLS. The one that must never be in the browser is service_role (or the new secret key).
Do the new publishable and secret keys change anything?
They change the format and allow rotation without touching the JWT, but the model is the same: publishable is public and goes through RLS; secret bypasses it and lives on the server. The old keys keep working until you disable them.
If Lovable or Bolt built my app, do I still have to open the Supabase dashboard?
Yes. The tool generates the schema, but RLS, policies, buckets and Auth settings are yours. Ten minutes with the three queries above and the Security Advisor tell you what state the project is in.