The Supabase RLS Security Checklist for 2026

Vasim Gujrati
Solutions Architect, AI & Platforms, Unico Connect
In this article
- Quick Answer
- Key Takeaways
- What Is Row Level Security and Why Does It Decide Everything
- What Happened in the CVE-2025-48757 Lovable Exposure
- Why Do New Tables Sometimes Ship Without RLS
- The Supabase RLS Security Checklist
- Let Supabase Audit Itself First
- How We Approach a Supabase Security Audit
- Frequently Asked Questions
Supabase gives your app a Postgres database that the browser can talk to directly. That is what makes it fast to build on, and it is also why row level security is the one setting you cannot get wrong. When a client reads the database with the public key, your RLS policies are all that keep a stranger out of your data. Below is every check we run on a Supabase project before it ships, with the exact SQL and requests you can run yourself today.
Quick Answer
The core rule of a Supabase RLS security checklist is that every table the browser can reach must have row level security enabled with a policy that matches your access logic, because the public key relies on RLS alone to protect data. Row level security is enabled automatically only when you create a table in the Table Editor. Tables created through raw SQL, a migration, or an AI coding tool get it only if that SQL turns it on, and missing or weak RLS on generated tables is the gap behind the 2025 Lovable exposure. Work through the checks in order. Confirm RLS is on for every public table and prove it with a direct request using the publishable key, or the legacy anon key. Keep the secret key off the client entirely, then harden storage, views, functions, and auth.
Key Takeaways
- RLS is your whole defense on any table the client can reach. With the publishable key, or the legacy anon key, the client queries Postgres directly, so a table without a correct RLS policy is readable by anyone with your project URL and that public key.
- Treat any table created outside the Table Editor as unprotected until you check it. Supabase enables RLS for you in the Table Editor but not for tables made in SQL, migrations, or AI tools, and real leaks come from that gap.
- Send one curl request with the publishable key or legacy anon key against your REST endpoint for each table that should be private. It tells you in seconds whether the table is exposed, and the CVE-2025-48757 researcher used the same method.
- The secret key belongs on the server only. It bypasses every RLS policy by design, so one copy of it in a client bundle undoes all of your work.
- Run the built in Security Advisor before you audit anything by hand. Supabase ships this database linter at no cost, and it flags disabled RLS, policy gaps, unsafe views, and exposed functions.
What Is Row Level Security and Why Does It Decide Everything
Row level security is a Postgres feature that attaches rules to a table so each database role only sees the rows a policy allows. Supabase leans on it because your frontend holds a public key, the publishable key or the legacy anon key, and uses it to query the database straight from the browser through the auto generated REST and GraphQL API. No backend sits in the middle deciding who sees what, so the database makes that call itself, using your RLS policies.
You get a database the client can query without writing an API layer, and in exchange the correctness of your access rules becomes the entire security model. Supabase says this plainly in the dashboard, where a table without RLS carries a warning that data access from the browser is safe only as long as you enable row level security.
When RLS is missing, the publishable key or legacy anon key works as a credential that can read, and often write, any table that lacks a policy. Anyone can find it, because it ships to every visitor in your JavaScript.
What Happened in the CVE-2025-48757 Lovable Exposure
The clearest documented example of this risk is CVE-2025-48757, an exposure affecting apps generated by the AI builder Lovable. The security researcher Matt Palmer disclosed it in 2025. He notified the vendor in March 2025 and published on 29 May 2025 after the disclosure window passed.
The numbers from his scan are often repeated wrongly, so they are worth stating exactly. Palmer scanned 1,645 projects from the Lovable public showcase and found 170 of them, roughly 10.3 percent, with inadequate row level security, exposing 303 endpoints in total. What leaked through those endpoints included names, emails, phone numbers, home addresses, payment information, and third party API keys. The severity scores for the CVE do not fully agree, so you should know both. The national vulnerability database lists it as 9.3, critical, while the advisory Palmer published scored it 8.26. Lovable has disputed the CVE, and its position is that customers own the security of their app data. If you want outside help with that, our roundup of the best Lovable development agencies covers what to ask about RLS audits.
Lovable generated apps that queried the database directly from the client with the public key and relied on RLS alone to protect the data. When the generated tables had no policy, the public key could pull entire tables. Any Supabase app fails the same way when a table reaches the client without a policy, so the mechanism matters more than the Lovable label. If your app was built fast, by an AI tool or otherwise, you are in exactly the situation this checklist is designed to catch. The 2025 Tea app breach was the same class of mistake, though that incident involved a misconfigured Firebase storage bucket rather than Supabase, and it is a reminder that direct client access to a data store is unforgiving when the rules are missing.
Why Do New Tables Sometimes Ship Without RLS
The most important technical detail in this checklist is that Supabase enables row level security automatically only when you create a table through the Table Editor in the dashboard. Create the same table with raw SQL, through the SQL editor, in a migration file, or with an ORM like Prisma, and RLS is off until you turn it on yourself. The Supabase docs state this directly and remind you to enable RLS yourself whenever you create a table in SQL.
AI built and migration driven apps leak for this reason. Tools that scaffold a schema for you almost always create tables in SQL, the path where RLS is off by default. A developer clicking through the Table Editor gets RLS without asking for it, while a code generator writing migrations gets it only if its SQL turns it on, and nothing in Supabase stops the app from shipping without it. Lovable now sets up basic RLS policies automatically and tells you to review and adjust them, since a policy that exists can still let the wrong rows through.
The Supabase RLS Security Checklist
Work through these in order. The first three catch real exposures, and the rest close the gaps around them.
How Do You Find Every Table Missing RLS
Run this in the SQL editor to list every table in the public schema and whether row level security is on.
select schemaname, tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity asc, tablename;
Any row where rowsecurity is false is reachable through the API with the publishable key or legacy anon key whenever the anon role holds a grant on that table. On projects created before 30 May 2026, Supabase gives anon that grant by default on every table added to the public schema before 30 October 2026. Enable RLS on each one, then confirm a policy exists, because a table with RLS enabled and no policy denies all access and will break your app in the opposite direction.
alter table public.your_table enable row level security;
How Do You Prove a Table Is Actually Protected
Do not trust the dashboard alone. Test the live API the way an attacker would, with nothing but your publishable key, or the legacy anon key, and the project URL. Either key goes in the apikey header, in place of YOUR_ANON_KEY in the example below.
curl "https://YOUR_PROJECT_REF.supabase.co/rest/v1/your_table?select=*" \
-H "apikey: YOUR_ANON_KEY"
A protected table returns an empty array or an error. If you get back real rows from a table that should be private, that table is exposed to the public internet right now. The CVE-2025-48757 projects were found with this same method, and each check takes seconds.
Is Your Secret Key Anywhere Near the Client
The secret key, like its legacy version the service_role key, uses the Postgres BYPASSRLS attribute. It skips every policy you have written, by design, so it exists only for trusted server code such as backends and edge functions. One copy in a client bundle makes your entire RLS layer irrelevant. Search your built JavaScript, mobile bundles, and git history for it. The command below finds the service_role name. Run it again with sb_secret_, the prefix of every new secret key, and check any long string starting with eyJ that is not your anon key, because legacy keys are JWTs in that format.
git grep -i "service_role" $(git rev-list --all) 2>/dev/null
The only Supabase keys that belong in client code are the publishable key and its legacy version, the anon key. Supabase now returns a 401 when a new secret key is used from a browser, a check the legacy service_role key does not have, but you should never rely on that as your control.
Are Your Policies Written Correctly
A table can have RLS on and a policy present and still be wrong. The two common mistakes are a policy that matches every row and a write policy that never checks what is being written. A policy with true as its condition lets every row through. USING decides which existing rows a user can see or change, and WITH CHECK decides which new or changed rows may be saved. An insert policy accepts only WITH CHECK, and an update policy without one reuses its USING condition, so write the WITH CHECK clause explicitly and tie it to the user.
create policy "Users read their own rows"
on public.profiles for select
using ((select auth.uid()) = user_id);
create policy "Users write their own rows"
on public.profiles for insert
with check ((select auth.uid()) = user_id);
Wrapping auth.uid() in a select lets Postgres cache it per statement, which keeps policies fast as tables grow. Because auth.uid() is null when there is no logged in user, check that the unauthenticated case behaves the way you expect.
Do Your Views and Functions Bypass RLS
Postgres views run with the permissions of their creator by default, which means a view can return rows the underlying table policy would have blocked. On Postgres 15 and later, set views to run as the caller instead.
alter view public.your_view set (security_invoker = true);
Functions marked security definer need the same care, because they run with elevated rights. Confirm that none of them are callable by the anon role unless you intend it, and pin the search_path on every function to avoid injection through a mutable path.
Is Your Storage Locked Down
Supabase Storage is governed by policies on the storage.objects table, the same RLS model as your database. Buckets are private by default, and a private bucket enforces policies on every operation including downloads. A public bucket serves any file to anyone who has the URL, so mark a bucket public only when the files in it are meant for the open web.
Are Your Edge Functions and Auth Hardened
Edge functions verify the caller JWT by default and return a 401 without a valid token, so make a function public only when you mean to. Finish with the account and auth settings that Supabase lists in its own production checklist. Turn on SSL enforcement, add network restrictions on the database, protect your Supabase account with multi factor authentication, enable email confirmations, and set the one time password expiry to an hour or less.
Let Supabase Audit Itself First
Before you run any of the above by hand, open the Security Advisor in your project dashboard. The Advisor is a database linter that Supabase runs for you, and it flags the problems this checklist targets, including row level security disabled on public tables, RLS enabled with no policy, security definer views, functions with a mutable search path, and sensitive columns exposed through the API. Nothing finds the obvious holes faster, and it is free, so use it as the first pass and this checklist as the deeper one.
How We Approach a Supabase Security Audit
We productionize apps built fast, including ones generated by AI tools, and a security pass is always the first thing we run. It follows the order above. We enumerate every table and its RLS state, prove exposure with live requests rather than trusting settings, pull the secret key out of anywhere a client can reach, and then work through storage, views, functions, and auth. Because we deliver under an ISO/IEC 27001:2022 certified information security management system, that audit is documented rather than ad hoc. If you have shipped a Supabase or Lovable app and you are not sure whether your data is protected, our MVP build and rescue service includes a free architecture and security review. It covers 1 to 2 business days of work from our team, and we share the initial findings with you. If you need a deeper audit, it is paid, and its scope depends on the scale of the project. Our Lovable development and rescue work covers this exact class of exposure.
Frequently Asked Questions
Is row level security enabled by default in Supabase?
Only when you create a table in the Table Editor in the dashboard. Tables created with raw SQL, in a migration, or by an AI coding tool have row level security only if that SQL enables it, and that gap is the most common reason an app ships with exposed data.
Can someone steal data with just the Supabase anon key?
Yes, if a table has no row level security policy. The publishable key and the legacy anon key it replaces are both public and ship to every visitor in your frontend code, and the only reason they are safe to expose is that RLS is supposed to guard what they can reach. On a table without a policy, either key can read and often write every row.
What is the difference between the anon key and the service_role key?
The anon key is the legacy public key meant for client code, and its access is limited by your RLS policies. The service_role key is the legacy version of the secret key, and it bypasses all RLS policies by design, so it must stay on the server only. Supabase is deprecating both legacy keys by the end of 2026, and the publishable key and the secret key that replace them split access the same way. A leaked secret or service_role key exposes your entire database regardless of how good your policies are.
How do I test whether my Supabase table is exposed?
Send a request to your REST endpoint with only the publishable key or legacy anon key, for example a curl request to the rest path for that table. If a table that should be private returns real rows, it is exposed to the public internet. Protected tables return an empty result or an error.
Does the Supabase Security Advisor catch every problem?
No, but it catches the common and dangerous ones, including disabled RLS, missing policies, unsafe views, and exposed functions, so always run it. It does not judge whether a policy matches your intended access logic, so a policy that is present but too permissive can still pass. Review your policies by hand as well.
We built our app with an AI tool. Is our data at risk?
It can be. AI builders create tables in SQL, which is the path where Supabase does not enable row level security for you, and they rely on RLS to protect data reaching the client. That combination, with missing or weak policies, caused the 2025 Lovable exposure. To find out where you stand, run the checks above, starting with a direct request using your publishable key or legacy anon key.




