In short
- Supabase's shared responsibility model leaves access management, your data and applying security controls to you, including acting on Security Advisor's alerts.
- The public key ships in your JavaScript, so any table, view, function or bucket the
anonrole can reach is open to anyone. RLS and grants are the lock, not the key. verify_jwtdoes not keep strangers out of an Edge Function: it accepts the public key. A function that calls a paid AI model or reads data must check the user in its own code.- Security Advisor catches many risky settings, but it reads your schema and settings. It cannot tell whether user A can read user B's rows, or what your JavaScript bundle exposes.
- UpGuard researchers found 16,326 Supabase databases with tables strangers could read, reported in September 2026, and blamed missing or ineffective RLS and misused public keys.
On this page
- Is Supabase secure?
- What Supabase is responsible for, and what you are
- The Supabase stack, and where apps built on it leak
- What a stranger can do with a misconfigured Supabase app
- What Supabase's Security Advisor catches, and what it misses
- How to check your Supabase app
- Fixes, with prompts for your coding agent
- The traps after the first fix
- Test the live app, not only the schema
- Supabase security checklist
- Questions
- Sources
Is Supabase secure?
Supabase does its share well. Customer data is encrypted at rest with AES-256 and in transit with TLS, and access tokens and keys are encrypted again at the application level before they are stored. The company is SOC 2 Type 2 compliant and ISO 27001 certified, offers HIPAA business associate agreements to Team and Enterprise customers, runs regular penetration tests with outside experts, and uses Cloudflare at the CDN level against denial of service. Paid projects are backed up every day.
None of that decides who can read your rows. Supabase's own security page puts the split in two sentences: Supabase secures the infrastructure, and you secure your application, meaning your RLS policies, API keys and access controls. Its shared responsibility model adds that you are always responsible for your account, access management, your data and applying security controls, and that acting on Security Advisor's alerts is your job. So the fair answer to "is Supabase secure" is: the platform is well run and audited, and your app is only as safe as its configuration.
That configuration mostly means row level security (RLS): a Postgres feature that attaches policies to a table, so each query only sees the rows a policy allows. On Supabase it is the main lock, because your frontend talks to the database directly.
What Supabase is responsible for, and what you are
Here is the split, area by area, taken from Supabase's security page, its shared responsibility model and its production security checklist.
| Area | Supabase handles | You handle |
|---|---|---|
| Infrastructure | Encryption at rest and in transit, daily backups on paid plans, DDoS protection, penetration tests | SSL enforcement and network restrictions for direct database connections |
| Data access | The Data API, the RLS engine, Security Advisor | RLS on every exposed table, the policies, the grants |
| Keys | Issuing keys and encrypting them at rest | Keeping secret and service_role keys out of browsers and repositories |
| Auth | The Auth server, rate limits, optional CAPTCHA | Email confirmations, link expiry, the redirect allow list, MFA for your users |
| Edge Functions | Running them, plus a platform JWT check (verify_jwt, on by default) | Checking who the caller is inside each function |
| Storage and Realtime | Authorization based on your policies | Private buckets, policies on storage.objects and realtime.messages |
| Your Supabase account | MFA, organization roles, MFA enforcement on the Pro, Team and Enterprise plans | Turning MFA on, keeping more than one owner |
| Third-party services | No monitoring of outages or performance issues in these integrations | SMTP, OAuth providers, external APIs your functions call |
The right-hand column is where generated apps leak. A coding agent can build a working app without touching any of it, and the app behaves the same either way: every screen shows the right data whether RLS is on or off.
The Supabase stack, and where apps built on it leak
A Supabase project is a Postgres database with services around it: an auto-generated Data API (REST through PostgREST at /rest/v1/, plus GraphQL when the pg_graphql extension is on), Auth, Storage, Realtime and Edge Functions. Most apps that AI builders generate call the Data API straight from the browser, with code like supabase.from('orders').select(), and have no server of their own in between.
To do that, the frontend carries a public key: the publishable key (sb_publishable_...) or, on older projects, the legacy anon key, which Supabase is deprecating by the end of 2026. Both make a signed-out caller the anon role, and a signed-in user the authenticated role. The secret key (sb_secret_...) and the legacy service_role key skip RLS and belong on a server only. The anon key guide explains why the public key is fine to ship and what it reaches.
| Surface | How a stranger reaches it | Where it usually leaks |
|---|---|---|
| Tables and views | /rest/v1/ with the public key from your JavaScript | RLS off, policies with using (true), views that skip the table's RLS |
| Database functions | /rest/v1/rpc/ with the public key | security definer functions that anon can call and that return other users' data |
| Storage | Bucket URLs and the Storage API | Private files in a public bucket, buckets anyone can list or upload to |
| Realtime | A WebSocket opened with the public key | Replicated tables without RLS, channels left public |
| Auth | The sign-up and sign-in endpoints | Policies that only check "signed in", confirmations off, broad redirect wildcards |
| Edge Functions | Their public URL | Functions that call a paid AI model or use the secret key without checking the caller |
| JavaScript bundle | View source in any browser | A service_role or sb_secret_ key, or other providers' secret keys |
What a stranger can do with a misconfigured Supabase app
Each of these starts with what any visitor already has: your app's address, its JavaScript and the public key inside it. Your table names are in that JavaScript too.
Read every row of a table
A profiles or orders table with RLS off answers any request that carries the public key. The REST API supports filters and paging, so reading one row and reading the whole table take the same effort. That is the pattern behind both studies above.
Change or delete rows
Under the old defaults, every table in public got select, insert, update and delete grants for anon and authenticated. Supabase began making explicit grants the default for new projects on 30 May 2026 and announced the change for existing projects from 30 October 2026, but it only covers new tables: tables that already existed keep their grants. On those tables, RLS off means writable by strangers.
Sign up, then read everyone's data
A policy like create policy ... to authenticated using (true) looks protective, since signed-out visitors get nothing. But if your app lets anyone sign up, a stranger creates an account in a minute and the policy hands them every row. Policies on user data have to compare a column to auth.uid(), the ID of the signed-in user. This is the database version of an IDOR vulnerability, and the most common access control bug we see in generated apps.
Spend your AI budget
An Edge Function that calls an AI model for your app has a public URL. The platform check, verify_jwt, is on by default, but Supabase documents that it accepts publishable and secret keys for migration compatibility, and that on its own it does not authenticate a caller who sends only an API key. The legacy anon key is itself a JWT signed for your project, the kind the check validates, so it passes too. Since the public key is in your JavaScript, a function that does not check the user in its code answers anyone. The unprotected AI endpoint guide covers limits and quotas.
Listen to live changes
Realtime pushes database changes to subscribed clients, and RLS decides who receives each row. A replicated table with RLS off streams its changes to anyone holding the public key. Supabase's checklist adds that RLS does not filter Postgres Changes DELETE events, and private channels for Broadcast and Presence are only enforced once you turn off Allow public access in the Realtime settings.
Catch a sign-in link
Magic links, password resets and OAuth sign-ins send the user back to your app with a token or a code in the URL. Supabase only redirects to addresses on your allow list, which accepts wildcards for preview deployments. Supabase recommends the exact URL path in production: a pattern broad enough to match a domain someone else controls can send that token to them.
What Supabase's Security Advisor catches, and what it misses
Supabase ships deterministic checks called Advisors. You can run them from the dashboard (Security Advisor), from the CLI with supabase db advisors, from the Supabase MCP server with get_advisors, or through the Management API. They come with every project, and you should run them before every launch. The security checks flag, among others:
- tables in
publicwith RLS disabled, and tables that have policies while RLS is still off; - overly permissive policies, and policies that read
user_metadata, which signed-in users can edit themselves; - views defined as
security definer, andsecurity definerfunctions thatanonorauthenticatedcan execute; - functions whose
search_pathis not pinned; - sensitive columns exposed through the API,
auth.usersexposed through a view, and public buckets that allow listing; - tables exposed through GraphQL, and anonymous sign-ins being enabled.
Supabase's production security checklist goes further, with database, account and Auth settings, and includes a read-only audit prompt you can hand to your coding agent.
What the Advisor cannot see is behaviour. It reads your schema and settings, so it does not know whether a policy that looks reasonable lets one user read another user's rows, whether an Edge Function checks its caller, whether a secret key ended up in your JavaScript bundle, or whether your redirect allow list matches a domain you do not own. Supabase also notes that some findings may be intentional, so each one needs a judgement. For the rest, you have to test the live app the way a stranger would.
How to check your Supabase app
Start from the outside, then go inside your project. Nothing below changes your data, and every request stays inside your own project.
- 1
Look at what your frontend exposes
Open your live app and the browser's developer tools. Search the loaded JavaScript for
supabase.co,sb_publishable_,sb_secret_andeyJ. Paste any key you find into the Supabase key checker: a publishable or anon key is expected, a secret orservice_rolekey is an emergency. - 2
Run Security Advisor
Open Security Advisor in the dashboard, or run
supabase db advisorswith the CLI. Fix every error, and read every warning against how your app is meant to work. - 3
List what the anon role can reach
Run the queries below in the SQL editor. The first lists tables without RLS and whether
anoncan read or write them. The second lists the functionsanoncan call,security definerones first. The third lists the tables Realtime streams, with their RLS status. - 4
Review the policies
Paste your policies into the RLS checker, or read
pg_policies. Every policy on user data should compare a column toauth.uid(). A condition oftrue, or one that only checks that the caller is signed in, needs a reason. - 5
Read the Auth and Realtime settings
In Authentication, check that email confirmations are on, that OTP and link expiry is 3600 seconds or less, and that the redirect allow list holds exact production URLs. In the Realtime settings, check whether Allow public access is still on.
- 6
Read every Edge Function
Open
supabase/config.tomland note the functions withverify_jwt = false. Then find, in every function, the line that checks the user. A function that reaches the secret key or a paid API without that check is open. - 7
Test with two accounts
Sign up as user A and user B in your own app. As A, try to load B's profile, orders or files by changing an ID in the request. If B's data comes back, a policy is missing its check on
auth.uid().
-- 1. Tables without RLS, and what anon can do with them
select c.relname as table_name,
has_table_privilege('anon', c.oid, 'select') as anon_can_read,
has_table_privilege('anon', c.oid, 'insert,update,delete') as anon_can_write
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind in ('r', 'p') and not c.relrowsecurity;
-- 2. Functions anon can call through /rest/v1/rpc/
select p.proname as function_name,
p.prosecdef as security_definer
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'
and has_function_privilege('anon', p.oid, 'execute')
order by security_definer desc, function_name;
-- 3. Tables Realtime streams, and whether RLS is on
select pt.schemaname, pt.tablename, c.relrowsecurity as rls_enabled
from pg_publication_tables pt
join pg_namespace n on n.nspname = pt.schemaname
join pg_class c on c.relnamespace = n.oid and c.relname = pt.tablename
where pt.pubname = 'supabase_realtime';public.Fixes, with prompts for your coding agent
Most fixes are a few lines of SQL. Ask your agent to write them as a migration, so they apply the same way in every environment, and keep the grants, RLS and policies in the same migration, as Supabase recommends.
Tables: RLS on, one policy per user and action
-- Before: any signed-in account reads every profile
-- create policy "Profiles are visible" on public.profiles
-- for select to authenticated using (true);
-- After: each user reads and edits only their own row
alter table public.profiles enable row level security;
drop policy if exists "Profiles are visible" on public.profiles;
create policy "Read own profile" on public.profiles
for select to authenticated using ((select auth.uid()) = id);
create policy "Update own profile" on public.profiles
for update to authenticated
using ((select auth.uid()) = id) with check ((select auth.uid()) = id);
revoke insert, update, delete on public.profiles from anon;auth.uid() in select lets Postgres evaluate it once per query.Audit every table in the public schema of my Supabase project. For each table:
enable row level security, write select, insert, update and delete policies that
compare the owner column to (select auth.uid()), and revoke from anon every
privilege the app does not need. Never use using (true) on user data, and never
read user_metadata in a policy. Put the grants, RLS and policies in one migration.
Show me the tables and policies before you apply it. Afterwards, run get_advisors
with type security and fix what it reports.The Supabase RLS guide has policies for the other common cases: team members, public read with owner write, admin roles.
Functions and views
Revoke execute from the API roles on any function the frontend does not call, set search_path = '' on every security definer function, and make views respect the caller's RLS with security_invoker (Postgres 15 and later). Supabase's docs say never to create a security definer function in an exposed schema.
revoke execute on function public.admin_stats() from anon, authenticated, public;
alter function public.get_team_members(uuid) set search_path = '';
alter view public.profile_cards set (security_invoker = true);Edge Functions
Check the caller in code, whatever verify_jwt says. Supabase's withSupabase wrapper from @supabase/server does it with auth: 'user' and gives your handler ctx.supabase, a client scoped to the caller's RLS. Use ctx.supabaseAdmin only for work that has to cross policies, and filter it by the caller's ID. Webhooks from Stripe or GitHub carry no Supabase credentials, so verify their signature instead, as the Stripe webhook guide shows.
import { withSupabase } from 'npm:@supabase/server@1'
export default {
fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => {
// ctx.supabase runs every query under the caller's RLS policies
const { data, error } = await ctx.supabase.from('orders').select('id, total')
if (error) return Response.json({ error: error.message }, { status: 400 })
return Response.json(data)
}),
}withSupabase answers 401 to a caller without a valid user token, before your handler runs.For every Supabase Edge Function in this repo, tell me who can call it today.
Functions called from the app must require a signed-in user (withSupabase with
auth 'user') and query through the RLS-scoped client. Keep verify_jwt = false only
for webhooks and service calls, and make those verify a signature or the secret
key. Add a per-user rate limit to every function that calls a paid AI API.Storage, Realtime, Auth and keys
- Storage: keep private files in private buckets, with policies on
storage.objectsthat limit each user to their own folder. The storage guide has the policy. - Realtime: turn off Allow public access, join channels with
private: true, and send sensitive changes through Broadcast with Realtime Authorization rather than Postgres Changes. - Auth: turn on email confirmations, keep OTP expiry at 3600 seconds or less, use exact redirect URLs in production, remove test phone numbers, and add CAPTCHA to sign-up if bots create accounts.
- Keys: if a
service_roleorsb_secret_key ever reached a browser or a public repository, follow the service role key guide now. Move clients to the publishable key before the anon key is retired, as the anon vs publishable key guide explains.
The traps after the first fix
- The grant hint. Under the new grants default, a new table answers with error
42501and a hint that spells out theGRANTto run. An agent that pastes it on a table where RLS is still off reopens the leak. Grant only after RLS and policies are in place. - Views. A view over a locked table runs with its owner's privileges unless it is a
security_invokerview, so it can hand the whole table toanon. - GraphQL. Supabase stopped enabling
pg_graphqlby default on new projects in May 2026, but older projects may still serve tables through it. The Advisor flags the tables it exposes. - Other exposed schemas. If your API settings expose more than
public, run every check on those schemas too. - Roles in `user_metadata`. Users can edit their own metadata, so a policy that reads a role from it lets them promote themselves. Keep roles in
app_metadataor in a table they cannot write. - New features. Each feature your agent ships can add tables, functions and buckets. Run the checks again after each one, not once before launch.
Test the live app, not only the schema
Reading SQL tells you what your policies say. An attack tells you what a stranger actually gets, signed out and then signed in as someone else. Keybrum runs more than 100 attacks, only against apps whose owner proved ownership. On a Supabase backend, it:
- reads every table it can reach with the public key, anonymously and then as a freshly created account, and keeps only counts, column names and one redacted row;
- probes inserts and updates in a way that cannot persist: each request is built to fail on a constraint and runs with a rollback, so a constraint error proves the policy let it through;
- calls exposed database functions, lists and downloads from buckets, tries uploads that can only collide with an existing file, and subscribes to Realtime changes;
- checks whether the Auth redirect accepts any site, whether a legacy JWT secret is a known default, and whether Edge Functions that look like AI endpoints answer without an account;
- looks for
service_roleandsb_secret_keys in your JavaScript, and for other providers' keys stored in readable rows.
Every finding comes with the request that worked, and your coding agent reads the fix through Keybrum's MCP server. Then Keybrum attacks again after the fix and, on a plan, on every deploy.
Supabase security checklist
- RLS is enabled on every table in every exposed schema.
- Every policy on user data compares a column to
auth.uid(), and nousing (true)exists without a reason. anonholds noinsert,updateordeletegrant the app does not need.- Views use
security_invoker, andanoncannot execute anysecurity definerfunction. - No policy reads a role from
user_metadata. - Private files sit in private buckets, with per-user policies on
storage.objects. - Realtime: Allow public access is off, and every replicated table has RLS.
- No
service_roleorsb_secret_key in the JavaScript bundle or the repository. - Every Edge Function checks its caller in code, and webhooks verify signatures.
- Email confirmations on, OTP expiry 3600 seconds or less, exact redirect URLs, no test phone numbers.
- MFA on your Supabase account, and more than one organization owner.
- SSL enforcement and network restrictions on for direct database connections.
- Security Advisor shows no finding you cannot explain.
- A two-account test shows that user A cannot read or change user B's data.
The vibe coding security checklist covers the rest of the app: secrets, headers, payments and AI endpoints.
Questions
Is Supabase secure?
The platform side is well run: data is encrypted at rest and in transit, and Supabase is SOC 2 Type 2 compliant and ISO 27001 certified. Whether your app's data is safe depends on your configuration: RLS on every exposed table, correct policies, private buckets and Edge Functions that check the caller. Most Supabase leaks come from that half.
Is Supabase SOC 2 compliant?
Yes. Supabase states it is SOC 2 Type 2 compliant and ISO 27001 certified, and Team and Enterprise customers can get the report and the certificate from the dashboard. Both cover how Supabase runs its platform, not the policies in your project, so they say nothing about whether your tables are locked.
Does Supabase have a security scanner?
Yes: Security Advisor runs deterministic checks on your schema and settings, from the dashboard, the CLI (supabase db advisors), the MCP server or the Management API. It reads configuration, so it cannot test what a stranger can do across accounts or what your JavaScript exposes. For that outside view, the free check shows your backend, exposed keys and table names.
What are the Supabase security best practices?
Enable RLS on every table and write policies that compare a column to auth.uid(). Keep the secret and service role keys on servers, check the caller inside every Edge Function, and keep private files in private buckets. Tighten Auth with email confirmations, short link expiry and exact redirect URLs, then run Security Advisor and a two-account test before each launch.
Is it safe to put the Supabase key in my frontend?
The publishable key, or the legacy anon key, yes: it is meant to be public, and your grants and policies decide what it reaches. The secret key and the service role key, never, because they skip RLS. Supabase refuses a secret key sent from a browser, but a leaked one still works from any other tool. The anon key guide shows how to tell the keys apart.
Does verify_jwt protect my Edge Function?
Not from strangers. Supabase documents that the check accepts publishable and secret keys, and your publishable key is in your JavaScript, so passing the check does not prove who the caller is. Check the user in the function's code, for example with withSupabase and auth: 'user'.
Who is responsible if my Supabase app leaks data?
Under Supabase's shared responsibility model, you are: access management, your data and applying security controls are always on your side. Supabase provides the tools, including RLS and Security Advisor, and runs the infrastructure underneath.
Sources
- 1.Shared Responsibility Model, Supabase Docs
- 2.Security at Supabase, Supabase
- 3.Production security checklist, Supabase Docs
- 4.Advisors, Supabase Docs
- 5.Authorization headers (verify_jwt), Supabase Docs
- 6.Securing Edge Functions, Supabase Docs
- 7.Breaking Change: Tables not exposed to Data and GraphQL API automatically, Supabase Changelog
- 8.Row Level Security, Supabase Docs
- 9.Realtime Authorization, Supabase Docs
- 10.Redirect URLs, Supabase Docs
- 11.API keys, Supabase Docs
- 12.Everything Everywhere: Systemic Data Exposure in Supabase Apps, UpGuard
- 13.Over 16,000 Supabase databases expose PII, passwords, auth tokens, BleepingComputer
- 14.CVE-2025-48757, NIST National Vulnerability Database