Supabase security: is Supabase secure, and what does your app still need?

Supabase secures the platform: data is encrypted at rest and in transit, the company is SOC 2 Type 2 compliant and ISO 27001 certified, and its Security Advisor flags risky settings. It does not decide who can read your data. Row level security on every exposed table, grants, storage policies, Auth settings and the code of your Edge Functions are yours, and that is where apps built on Supabase leak.

The team behind Keybrum's attacksUpdated 17 min read

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 anon role can reach is open to anyone. RLS and grants are the lock, not the key.
  • verify_jwt does 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?

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.

AreaSupabase handlesYou handle
InfrastructureEncryption at rest and in transit, daily backups on paid plans, DDoS protection, penetration testsSSL enforcement and network restrictions for direct database connections
Data accessThe Data API, the RLS engine, Security AdvisorRLS on every exposed table, the policies, the grants
KeysIssuing keys and encrypting them at restKeeping secret and service_role keys out of browsers and repositories
AuthThe Auth server, rate limits, optional CAPTCHAEmail confirmations, link expiry, the redirect allow list, MFA for your users
Edge FunctionsRunning them, plus a platform JWT check (verify_jwt, on by default)Checking who the caller is inside each function
Storage and RealtimeAuthorization based on your policiesPrivate buckets, policies on storage.objects and realtime.messages
Your Supabase accountMFA, organization roles, MFA enforcement on the Pro, Team and Enterprise plansTurning MFA on, keeping more than one owner
Third-party servicesNo monitoring of outages or performance issues in these integrationsSMTP, OAuth providers, external APIs your functions call
Sources: Supabase security page, shared responsibility model, production security checklist and Edge Functions docs.

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.

SurfaceHow a stranger reaches itWhere it usually leaks
Tables and views/rest/v1/ with the public key from your JavaScriptRLS off, policies with using (true), views that skip the table's RLS
Database functions/rest/v1/rpc/ with the public keysecurity definer functions that anon can call and that return other users' data
StorageBucket URLs and the Storage APIPrivate files in a public bucket, buckets anyone can list or upload to
RealtimeA WebSocket opened with the public keyReplicated tables without RLS, channels left public
AuthThe sign-up and sign-in endpointsPolicies that only check "signed in", confirmations off, broad redirect wildcards
Edge FunctionsTheir public URLFunctions that call a paid AI model or use the secret key without checking the caller
JavaScript bundleView source in any browserA 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.

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 public with 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, and security definer functions that anon or authenticated can execute;
  • functions whose search_path is not pinned;
  • sensitive columns exposed through the API, auth.users exposed 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. 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_ and eyJ. Paste any key you find into the Supabase key checker: a publishable or anon key is expected, a secret or service_role key is an emergency.

  2. 2

    Run Security Advisor

    Open Security Advisor in the dashboard, or run supabase db advisors with the CLI. Fix every error, and read every warning against how your app is meant to work.

  3. 3

    List what the anon role can reach

    Run the queries below in the SQL editor. The first lists tables without RLS and whether anon can read or write them. The second lists the functions anon can call, security definer ones first. The third lists the tables Realtime streams, with their RLS status.

  4. 4

    Review the policies

    Paste your policies into the RLS checker, or read pg_policies. Every policy on user data should compare a column to auth.uid(). A condition of true, or one that only checks that the caller is signed in, needs a reason.

  5. 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. 6

    Read every Edge Function

    Open supabase/config.toml and note the functions with verify_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. 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().

sql
-- 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';
Read-only. Add other schemas to the filters if your API exposes more than 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

sql
-- 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;
The before policy is commented out so the migration runs as is. Wrapping auth.uid() in select lets Postgres evaluate it once per query.
text
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.
Prompt for your coding agent.

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.

sql
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.

ts
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)
  }),
}
Pattern from Supabase's Securing Edge Functions guide: withSupabase answers 401 to a caller without a valid user token, before your handler runs.
text
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.
Prompt for your coding agent.

Storage, Realtime, Auth and keys

  • Storage: keep private files in private buckets, with policies on storage.objects that 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_role or sb_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 42501 and a hint that spells out the GRANT to 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_invoker view, so it can hand the whole table to anon.
  • GraphQL. Supabase stopped enabling pg_graphql by 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_metadata or 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_role and sb_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 no using (true) exists without a reason.
  • anon holds no insert, update or delete grant the app does not need.
  • Views use security_invoker, and anon cannot execute any security definer function.
  • 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_role or sb_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. 1.Shared Responsibility Model, Supabase Docs
  2. 2.Security at Supabase, Supabase
  3. 3.Production security checklist, Supabase Docs
  4. 4.Advisors, Supabase Docs
  5. 5.Authorization headers (verify_jwt), Supabase Docs
  6. 6.Securing Edge Functions, Supabase Docs
  7. 7.Breaking Change: Tables not exposed to Data and GraphQL API automatically, Supabase Changelog
  8. 8.Row Level Security, Supabase Docs
  9. 9.Realtime Authorization, Supabase Docs
  10. 10.Redirect URLs, Supabase Docs
  11. 11.API keys, Supabase Docs
  12. 12.Everything Everywhere: Systemic Data Exposure in Supabase Apps, UpGuard
  13. 13.Over 16,000 Supabase databases expose PII, passwords, auth tokens, BleepingComputer
  14. 14.CVE-2025-48757, NIST National Vulnerability Database

Your app is public. So are its mistakes.

Check what any attacker can already see in it, free, in ten seconds. The full attack, with proof and fixes: €49 once, or €29 a month on a plan.

or continue with Google or GitHub