On this page7 sections
Supabase row level security (RLS) is a Postgres feature that checks every row against rules you write, called policies, before anyone can read or change it. It is what keeps a Supabase app’s data private, because the app’s public key ships to every visitor’s browser. On a table without RLS, anyone who copies that key can read, edit and delete every row.
We proved it on a local copy of Supabase’s setup. A stranger holding only the public key read two users’ phone numbers and renamed them both, and one command plus three policies shut that down. This guide shows the real output, then how to check every table in your own project, including apps built with Lovable or Bolt.
- The anon or publishable key is meant to be public. Row level security is what makes that safe.
- A table without RLS in an exposed schema is open to anyone with your project URL and key, for reading, editing and deleting.
- Turning RLS on blocks everything. Policies then let each person reach only their own rows.
- The secret or service_role key skips RLS entirely, so it belongs on a server, never in the app.
- Check every table with the Security Advisor, one SQL query, or a request made with your public key. Lovable and Bolt have built-in scans too.
What is row level security?
Without RLS, Postgres checks only whether a role may use a table at all. With RLS, it also checks each row. A policy is a condition such as “the row belongs to the signed-in user”, and Supabase’s auth.uid() function returns that user’s ID for exactly this job.
RLS works alongside your login, not instead of it. Login proves who someone is; policies decide what they may touch. Our guide to adding login to your app covers the first half.
Why is my Supabase anon key public, and is that OK?
Yes, it is designed to be public, and it is safe only when RLS is on for every table the API exposes. The key just identifies your project. Supabase maps each request to a Postgres role, anon for visitors and authenticated for signed-in users, and your policies decide what that role may reach.
Checked on September 28, 2026, from Supabase’s API keys docs:
| Key | Looks like | Where it may live | Row level security |
|---|---|---|---|
| Publishable key | sb_publishable_... | Anywhere: web pages, mobile apps, source code | Applies |
| Legacy anon key | A long JWT | Same as the publishable key | Applies |
| Secret key | sb_secret_... | Servers and backend functions only | Skipped: it runs as service_role, which bypasses RLS |
| Legacy service_role key | A long JWT | Servers only | Skipped |
Two details from the same page are worth knowing. A secret key refuses to work from a browser: Supabase checks the User-Agent header and answers 401 Unauthorized. And Supabase says it is deprecating the legacy anon and service_role keys by the end of 2026. If a secret key ever reached your frontend, rotate it; our guide to keeping API keys safe covers how.
We tested it: an open table, then a locked one
We rebuilt the parts of a Supabase project that RLS depends on, in plain PostgreSQL 16.13. That meant the anon, authenticated and service_role roles, the default grants that hand every new table in public to those roles, and auth.uid(), all copied from Supabase’s own source. Each request ran the way Supabase’s Data API runs one: in a transaction, as the right role, with the user’s token details set.
The table is the kind an AI builder writes in a SQL migration, with two users, Ada and Bob:
create table public.profiles (
id uuid primary key references auth.users (id),
full_name text not null,
phone text
);First, a stranger with nothing but the public key, before RLS:
full_name | phone
--------------+------------------
Ada Lovelace | +44 20 7946 0018
Bob Marley | +1 555 0100
(2 rows)The same stranger then ran update public.profiles set full_name = 'hacked', and 2 rows changed. That is the whole attack: no password and no exploit, just the key from your site’s JavaScript.
Then one command and three policies:
alter table public.profiles enable row level security;
create policy "Users can read their own profile"
on public.profiles for select
to authenticated
using ((select auth.uid()) = id);
create policy "Users can create their own profile"
on public.profiles for insert
to authenticated
with check ((select auth.uid()) = id);
create policy "Users can update their own profile"
on public.profiles for update
to authenticated
using ((select auth.uid()) = id)
with check ((select auth.uid()) = id);Here is every request we ran, at each stage:
| Request | RLS off | RLS on, no policies | RLS on, three policies |
|---|---|---|---|
| A stranger reads the table | Both rows | Nothing | Nothing |
| The stranger renames everyone | 2 rows changed | 0 changed | 0 changed |
| Ada, signed in, reads the table | Both rows | Nothing | Her own row |
| Ada renames everyone | Not run | Not run | 1 row changed: her own |
With the policies in place, Ada could not create a profile under another user’s ID, or move her own row onto one. Both attempts failed with new row violates row-level security policy for table "profiles".
Notice the middle column. RLS with no policies blocks everything, so your app will look broken, and that is the moment people switch RLS off again. Add policies instead. Our guide to choosing a database ran a shorter version of this test; this one adds the stranger’s view, writes and the mistakes below.
We also ran Supabase’s own Security Advisor checks against the test database with the Supabase CLI 2.84.2. With RLS off, they reported an ERROR, rls_disabled_in_public, for public.profiles. After the fixes, the only finding left was an INFO note about a newer table with RLS on and no policy yet.
How do I check every table in my Supabase project?
Run three checks, in this order: the Security Advisor, one SQL query, and a request made with nothing but your public key. Only test projects you own.
Open the Security Advisor
In your project’s dashboard, open Security Advisor and treat every ERROR as urgent. Three matter most for RLS.
rls_disabled_in_publicis a table anyone with your project URL can read, edit and delete.security_definer_viewis a view that skips RLS.rls_references_user_metadatais a policy that trusts data users can edit. From a terminal,supabase db advisorsruns the same checks.List every table and its policies
Paste this into the SQL editor. Any table with
rls_onset tof(false) is open to the public key.SQL select c.relname as table_name, c.relrowsecurity as rls_on, count(p.polname) as policies from pg_class c join pg_namespace n on n.oid = c.relnamespace left join pg_policy p on p.polrelid = c.oid where n.nspname = 'public' and c.relkind in ('r', 'p') group by c.relname, c.relrowsecurity order by rls_on, table_name;On our test database, before the fix, it printed
profiles | f | 0.Try it as a stranger
Copy your project URL and publishable key from the dashboard, then ask for a table the way anyone could. This is the request format from Supabase’s docs; we did not run it, because it needs a real project.
Terminal curl 'https://<PROJECT_REF>.supabase.co/rest/v1/profiles?select=*' \ -H "apikey: <PUBLISHABLE_KEY>"For a private table, an empty list,
[], is what you want. Real rows mean anyone on the internet can read them.
How do I check the security rules on a Lovable or Bolt app?
Use each builder’s own scan first, then confirm with the three checks above. The scans help, and neither claims to catch everything.
Lovable runs a Quick scan automatically when you open the publish dialog, covering database access rules among other things, and offers a manual Deep scan. Its docs say these tools cannot guarantee complete security.
Bolt has a Security Audit under the database icon, then Security. It flags a missing RLS policy or a permission that is too open, and offers an Ask Bolt to fix button.
After any automatic fix, run the checks again, because an AI-written policy can be wrong too. Missing RLS is behind two of the best-documented leaks from AI-built apps, both listed in our log of AI agent incidents.
Five RLS mistakes that still leak data
Turning RLS on is step one. These are the ways data still gets out, four of them reproduced in our test.
An always-true policy on private data.
using (true)for signed-in users looks safe, but anyone can sign up. In our test, Ada could read Bob’s phone number again. The advisor in the CLI we ran did not flag it: its always-true check (0024) skips read policies on purpose, sinceusing (true)is the normal way to make a table readable by everyone. Only reading your policies catches this one.A view over a protected table. Views run with their creator’s permissions unless told otherwise, so they skip RLS. Our stranger read both profiles through a view with RLS on, and got nothing after
alter view public.profile_directory set (security_invoker = true). The advisor flagged it as an ERROR.The secret key in the app. The
service_rolerole bypasses RLS. In our test it read every row with all policies in place.Policies that trust user metadata. Signed-in users can edit their own
raw_user_meta_data, so a policy that checks a role stored there can be talked around. Keep roles in a table users cannot write.New tables born open. Tables made in the Table Editor get RLS automatically; tables created with SQL, including migrations an AI tool writes, do not. Supabase’s docs include an event trigger that turns RLS on for every new table in
public. In our test, the next table created had RLS on from the start.
Supabase also says it is changing the platform default so new tables are no longer granted to the API roles automatically, making exposure opt-in. Until your project has that, assume every new table is public.
One more place the secret key hides: an AI agent connected to your database through MCP. A demo in 2025 showed an agent using Supabase’s MCP server with the service_role key leaking private tokens; see MCP security risks. For the other common holes in AI-written apps, see securing AI-generated code.
FAQ
Is it safe to expose the Supabase anon key?
Yes, if row level security is on for every table the API exposes and your policies are right. The key identifies your project; the policies decide what each request can see.
Is my Supabase database public?
Any table in an exposed schema without RLS is. Run the Security Advisor or the SQL query above. A table in public with RLS off can be read and changed by anyone with your public key.
What happens if RLS is on but there are no policies?
Nothing gets through the API. Reads come back empty and writes change nothing, as our test showed. Add a policy for each action you want to allow.
Does row level security slow down my app?
It adds a check to every query. Supabase recommends wrapping auth.uid() in a select, as in the policies above, so it runs once per query, and indexing every column your policies filter on.
Does the service role key bypass RLS?
Yes. Secret and service_role keys run as the service_role role, which bypasses every policy. Use them only on a server.
- The public key is fine to expose; a table without RLS is not.
- RLS on with no policies blocks everything. Add a policy per action instead of switching RLS off.
- Keep the secret key on servers, and out of AI agents that do not need it.
- Check with the Security Advisor, one SQL query and a request as a stranger, after every change.
Read next: how to keep API keys safe, and the AI agent incidents that started with an open table.
- Row Level Security, Supabase Docs, accessed September 2026
- API keys, Supabase Docs, accessed September 2026
- Securing your data, Supabase Docs, accessed September 2026
- Securing your API, Supabase Docs, accessed September 2026
- Tables and data, Supabase Docs, accessed September 2026
- Event triggers, Supabase Docs, accessed September 2026
- Advisors, Supabase Docs, accessed September 2026
- Build an API route in less than 2 minutes, Supabase Docs, accessed September 2026
- Initial schema: roles and default grants, supabase/postgres on GitHub
- Auth helper functions: auth.uid() and auth.role(), supabase/auth on GitHub
- Transactions, PostgREST documentation
- Supabase MCP security: how prompt injection leaked private tables, General Analysis, July 2025
- Security overview, Lovable Documentation, accessed September 2026
- Database: security settings, Bolt, accessed September 2026




