pwnmyvibecode_

Is your Supabase app secure?

Supabase is a secure platform: it runs Postgres for you, handles auth, and encrypts traffic. Your app is secure when every exposed table has row level security with sensible policies, and the service_role or secret key never leaves your server.

Updated

Check your live site now

Free, no signup, read only. A grade and plain fixes in seconds.

What Supabase handles, and what is up to you

Supabase runs the database, the auth server, storage and the REST and realtime APIs in front of them. It hashes passwords, signs session tokens and serves everything over TLS. You do not have to build any of that.

What it cannot do is guess your access rules. Supabase exposes tables in the public schema through an API that the browser calls directly with the anon or publishable key. That key is public on purpose. Row level security (RLS) policies decide what each request can see. Without them, the key is enough to read the whole table.

So the honest answer to whether Supabase is safe: the platform, yes. A Supabase app is only as safe as its policies and its key handling.

The two keys, and which one can be public

  • Public: the anon key (a JWT with role anon) or the newer publishable key (starts sb_publishable_). Fine in your front end, as long as RLS is doing its job.
  • Secret: the service_role key (a JWT with role service_role) or the newer secret key (starts sb_secret_). It bypasses RLS completely. It belongs in server code, Edge Functions or your host's server environment variables, and nowhere else.

The legacy keys look alike, both long strings starting eyJ. The difference is the role claim inside. Decode the middle part of the key on your own machine and read the role field to see which one you have. Don't paste a key that might be service_role into a website. If service_role ever shipped to a browser, replace it with a new secret key and disable the legacy keys.

Supabase security mistakes to look for

  • Tables created with SQL (including by an AI agent) without enabling RLS. Tables made in the dashboard Table Editor get RLS on by default. Tables made with plain SQL do not.
  • Policies that say using (true) on user data, which lets anyone read it.
  • An insert policy with no with check clause tying the new row to auth.uid(), so a user can write rows owned by someone else.
  • Views over protected tables. A view runs with its owner's rights unless it is created with security_invoker, so it can hand out rows the table's RLS would block.
  • SECURITY DEFINER functions callable by anon that return or change data without their own checks.
  • Public storage buckets for private files, or storage policies that let any signed-in user read every object.
  • The service_role key in a NEXT_PUBLIC_ or VITE_ environment variable, which puts it in the JavaScript bundle.

How to fix RLS in Supabase

Turn RLS on, then add a policy for each thing users need to do. With RLS on and no policies, the anon and authenticated roles can see nothing, which is a safe default while you work out the rules.

Supabase SQL editor
alter table public.profiles enable row level security;

create policy "Users read own profile"
on public.profiles for select
to authenticated
using ((select auth.uid()) = id);

create policy "Users update own profile"
on public.profiles for update
to authenticated
using ((select auth.uid()) = id)
with check ((select auth.uid()) = id);

To list which public tables still have RLS off:

Supabase SQL editor
select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;
prompt for your agent
List every table, view and function in the public schema of my Supabase project. For each table, enable row level security and write policies so a signed-in user can only select, insert, update and delete rows where the owner column equals auth.uid(). Recreate views with security_invoker = true. Make sure the service_role or sb_secret_ key is only used in server code. Show me the SQL first.

Supabase security checklist

  1. Run Security Advisor in the dashboard (Advisors, Security Advisor) and clear every error.
  2. Confirm RLS is on for every table in the public schema, using the query above.
  3. Read each policy on tables with personal or paid data. No using (true) unless the data is meant to be public.
  4. Check that inserts and updates have with check clauses.
  5. Search your front end code and env files for service_role, sb_secret_ and SUPABASE_SERVICE_ROLE_KEY with a public prefix.
  6. Set private buckets to private and give them owner-based storage policies.
  7. In Auth settings, restrict redirect URLs to your own domains.

How to test your live Supabase app from outside

Request a private table using only the anon key from your own site, with no login:

terminal
curl 'https://YOUR-PROJECT.supabase.co/rest/v1/orders?select=*&limit=5' \
  -H 'apikey: YOUR_ANON_KEY'

Rows in the response mean the table is readable by anyone. An empty list or a permission error means RLS or grants blocked it.

An outside scan like ours checks the other half. It reads your page and JavaScript bundles for a leaked service_role JWT (it decodes the role claim, so the public anon key is not flagged) or an sb_secret_ key, plus other secret keys, public .env and .git files, headers, cookies and CORS. It cannot see your database or your policies, so it cannot confirm RLS. That part is the Security Advisor and the curl test.

Questions

Is Supabase safe for production?

Yes, as a platform. Safety depends on your configuration: RLS on every exposed table, policies that match your intent, and secret keys kept on the server.

Is it safe to put the Supabase anon key in my front end?

Yes. It is designed to be public, and it only has the access your RLS policies give the anon and authenticated roles. The service_role or sb_secret_ key is the one that must stay private.

What happens if my service_role key leaked?

Anyone with it can read and change every table, ignoring RLS. Replace it straight away: create a new secret key in the Supabase dashboard, move your server code to it, then disable the legacy keys. After that, find out how it reached the browser.

Does RLS slow my app down?

Simple policies that compare a column to auth.uid() are cheap, especially with an index on that column. Wrapping auth.uid() in a select, as in the example above, lets Postgres evaluate it once per query.