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 what makes row level security the one setting you cannot get wrong. When a client reads the database with the public key, the only thing standing between a stranger and your data is your RLS policies. This checklist walks through 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 Supabase RLS security checklist has one rule at its core. 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 do not get it, which is the exact gap behind the 2025 Lovable exposure. Work through the checks below in order. Confirm RLS is on for every public table, prove it with a direct request using the anon key, keep the secret key off the client entirely, and then harden storage, views, functions, and auth.
Key Takeaways
- Row level security is the whole defense. With the public anon key the client queries Postgres directly, so a table without a correct RLS policy is readable by anyone with your project URL.
- The dangerous default is not a bug, it is a gap. Supabase enables RLS for you in the Table Editor but not for tables made in SQL, migrations, or AI tools, and that is where real leaks come from.
- Prove it, do not assume it. A single curl request with the anon key against your REST endpoint tells you in seconds whether a table is exposed. That is the same method the CVE-2025-48757 researcher used.
- 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. Supabase ships a database linter that flags disabled RLS, policy gaps, unsafe views, and exposed functions, and it costs nothing to check.
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 for a specific reason. Your frontend holds a public key, the anon key, and uses it to query the database straight from the browser through the auto generated REST and GraphQL API. There is no backend in the middle deciding who sees what. The database itself makes that call, and it makes it using your RLS policies.
This is the trade. 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 anon key is not a locked door. It is a working 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 illustration of the risk is a documented one. In 2025 the security researcher Matt Palmer disclosed CVE-2025-48757, an exposure affecting apps generated by the AI builder Lovable. He notified the vendor in March 2025 and published on 29 May 2025 after the disclosure window passed.
The numbers are worth stating exactly because they are often repeated wrongly. Palmer scanned 1,645 projects from 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 scores assigned to the CVE do not fully agree, and you should know both. The national vulnerability database lists it as 9.3, critical, while Palmer own advisory scored it 8.26. Lovable has disputed the CVE, and its position is that customers own the security of their app data.
The mechanism matters more than the label. 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. This is not a Lovable specific flaw so much as the natural failure mode of any Supabase app where a table reaches the client without a policy. If your app was built fast, by an AI tool or otherwise, it is exactly the situation this checklist is designed to catch. It is the same class of mistake as the 2025 Tea app breach, though that incident involved a misconfigured Firebase storage bucket rather than Supabase, 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
Here is the single most important technical detail on this page. 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 say exactly this, reminding you to enable RLS yourself when you create a table in SQL.
That is why AI built and migration driven apps are the ones that leak. The tools that scaffold a schema for you almost always create tables in SQL, which is the path that does not protect them by default. A developer clicking through the Table Editor is quietly protected. A code generator writing migrations is not, and nothing stops the app from shipping.
The Supabase RLS Security Checklist
Work these in order. The first three are the ones that catch real exposures. 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 anon key. Enable it 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 public anon key and the project URL.
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. This is precisely the method used to find the CVE-2025-48757 projects, and it takes seconds.
Is Your Secret Key Anywhere Near the Client
The secret key, historically called 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.
git grep -i "service_role" $(git rev-list --all) 2>/dev/null
The only Supabase key that belongs in client code is the publishable anon key. Supabase now returns a 401 when a secret key is used from a browser, 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 using true as its condition allows everything. A policy with only a USING clause filters reads but leaves inserts and updates unconstrained, so add a WITH CHECK clause for writes.
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. Make sure the unauthenticated case behaves the way you expect, since auth.uid() is null when there is no logged in user.
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);
The same care applies to functions marked security definer, which 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 genuinely 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. Only make a function public deliberately. Then 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. It is a database linter that Supabase runs for you, and it flags the exact 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. It is the fastest way to find the obvious holes, and it is free. Treat it as the first pass and this checklist as the thorough 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. The pattern is the one above. 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, 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 audit, and 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 do not have row level security until you enable it yourself. This 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 anon key is public and ships to every visitor in your frontend code. It is safe to expose only because RLS is supposed to guard what it can reach. On a table without a policy, that 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 public key meant for client code, and its access is limited by your RLS policies. The service_role or secret key bypasses all RLS policies by design and must stay on the server only. A leaked secret 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 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?
It catches the common and dangerous ones, including disabled RLS, missing policies, unsafe views, and exposed functions, and you should 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. Manual review of your policies is still needed.
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 row level security is not enabled automatically, and they rely on RLS to protect data reaching the client. That combination is what caused the documented 2025 exposures. Run the checks in this article, starting with a direct request using your anon key, to find out.




