The Supabase Security Audit Checklist Before You Ship
A complete supabase security audit checklist: RLS on every table, storage buckets, key handling, auth config, and the SECURITY DEFINER functions attackers love.
You're about to ship. The feature works, the demo looks great, and some part of you is asking whether you actually checked the security side or just assumed your AI builder handled it. That's the right instinct. A real supabase security audit isn't one dashboard toggle: it's five separate surfaces, and each one fails in its own quiet way.
This post is that audit, laid out as a checklist you can run in one sitting: tables and RLS, storage buckets, key handling, auth configuration, and database functions. That last one is the surface most audits skip, and it's a common way apps leak data even when every table looks locked down.
Treat this as a routine, not a one-time gate. Your AI ships new tables, new buckets, and new functions on every deploy: each one needs the same five checks before it goes live, not just before launch day.
⚡ TL;DR
- A full audit covers five surfaces: table RLS, storage buckets, key handling (anon vs service_role), auth config, and database functions/RPCs: checking only one tells you nothing about the others.
- The most-skipped surface is
SECURITY DEFINERfunctions: a database function that runs with elevated privilege and can be called byanon, quietly bypassing every RLS policy you wrote.- Run this checklist before every ship, not just before launch. A clean audit last month says nothing about the table your AI added yesterday.
The five surfaces of a real Supabase audit
Most self-checks stop at "is RLS on?" That's necessary but not sufficient. A Supabase project has five distinct places where access control lives, and a leak in any one of them undoes the work you did on the others.
- Row Level Security: who can read or write which rows, per table.
- Storage buckets: who can fetch which files.
- API keys: which key does what, and where each one is allowed to live.
- Auth configuration: the settings around who can become a "logged-in user" in the first place.
- Database functions and RPCs: custom server-side logic that can quietly ignore all of the above.
Skipping any one of these and calling it "audited" is how a project ends up with airtight RLS and a wide-open function that reads every row anyway. Go through all five, in order, every time you audit.
1. RLS on every table
This is the surface most people already know to check, so we won't re-derive the full method here (testing RLS the right way covers it in depth with the three personas that matter: anon (no login), owner (a user reading their own row), and other-user (a different logged-in user trying to read someone else's row)).
The audit-checklist version is short: for every table holding user data, confirm RLS is enabled and that an anonymous or other-user query against it returns nothing private. A green "RLS enabled" badge in the dashboard is not proof. A policy of USING (true) shows that same badge and still leaks every row. If you haven't run the three-persona test recently, do it now before moving to the next surface. And do it per table: one protected table tells you nothing about the next one your AI added last week.
2. Storage bucket exposure
Files don't live in your tables, so table RLS does nothing for them. Supabase Storage has its own access model, and it's easy to lock down every row in your database while a bucket of user-uploaded IDs or receipts sits open on the public web.
The short version, covered in full in Supabase storage bucket security: check whether each bucket is public or private, confirm private buckets have a real per-user policy (not a blanket rule), and grab a file URL from your app's network traffic to test it in an incognito window. If a sensitive file loads for a logged-out stranger, that bucket needs to move to private with signed URLs.
For this audit, the question to ask per bucket is simple: would I be comfortable with these exact files sitting on the open internet forever? For a logo, sure. For a folder of receipts or ID scans, no.
3. Key handling: anon vs service_role
Supabase gives you two keys with opposite jobs, and mixing them up is one of the most common (and most dangerous) mistakes an AI builder makes. The anon key is public by design and respects RLS; finding it in your browser network tab is not a leak. The service_role key bypasses RLS entirely and must never appear in client code, a public repo, or a shipped JS bundle.
We cover how to tell them apart (the role claim inside the JWT) and what to do if the wrong one leaked in service_role vs anon key. For the audit checklist, do two things: search your codebase and your live bundle for service_role, and confirm the only places that key appears are server-side: Edge Functions, backend code, admin scripts. If you find it anywhere a browser can reach, rotate it immediately and treat it as a live incident, not a cleanup task.
4. Auth configuration sanity check
This is the surface that's easy to forget because it lives in dashboard settings rather than code. A few misconfigurations here don't show up in a demo but matter the moment real users sign up.
- Email confirmation. Check Authentication → Settings for "Confirm email." If it's off, anyone can create an account with an email they don't own and start using your app immediately (fine for a quick prototype, a problem for anything handling real user data or payments).
- Redirect URLs. Auth flows (magic links, OAuth, password reset) send users to a URL you configure. If your allowed redirect URLs are still set to a wildcard or a leftover
localhostentry from development, an attacker can craft a link that redirects a legitimate auth token to a domain they control. Trim this list to your actual production domains. - Site URL. Make sure it points at your real production app, not a preview or staging URL left over from setup. A stale Site URL can leak confirmation links to the wrong place.
- JWT expiry. Confirm your access token lifetime is a reasonable window (Supabase defaults are sane) rather than something an early "just make auth work" prompt stretched out to weeks.
None of these show up when you're testing as yourself, logged in, on your own machine. They only bite in production, with real accounts, which is exactly why they belong in a pre-ship checklist rather than a one-time setup step you never revisit.
5. Database functions and RPCs: the SECURITY DEFINER footgun
Here's the surface most audits miss entirely, and it's worth naming explicitly because it's a real, common Supabase pattern: SECURITY DEFINER functions callable by anon.
Postgres functions normally run with the privileges of whoever calls them, so a function called by an anonymous visitor is still subject to that visitor's RLS restrictions. A function marked SECURITY DEFINER is different: it runs with the privileges of the function's creator instead, which in practice usually means it runs with elevated, RLS-bypassing access. AI builders reach for SECURITY DEFINER constantly, because it's the easy way to make a cross-table lookup or an aggregate query "just work" without wrestling with policies.
The footgun is when that function is also exposed as a callable RPC (Supabase auto-exposes database functions at /rest/v1/rpc/function_name) and the anon role has execute permission on it. At that point, RLS on the underlying tables is irrelevant. Anyone can call the function directly and get back whatever it returns, policies or not.
-- Find SECURITY DEFINER functions in your schema
select p.proname, p.prosecdef
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.prosecdef = true;
-- Check which roles can execute a given function
select grantee, privilege_type
from information_schema.role_routine_grants
where routine_name = 'your_function_name';
For each SECURITY DEFINER function that turns up, ask two questions: does it need to bypass RLS at all, or would a normal function plus a correct policy do the job? And if it truly needs elevated privilege, does anon (or even authenticated) really need execute access, or should this only be callable from a trusted server context? Revoke execute from public/anon on anything that shouldn't be reachable by a stranger:
revoke execute on function your_function_name from anon, public;
A function like this is easy to miss because it doesn't look like a leak in the dashboard: your tables all show RLS enabled, your buckets are private, your keys are clean. The leak lives one layer up, in a piece of custom logic nobody thought to re-check. This isn't hypothetical: see Supabase RPC leak hits Capgo for a real case study of exactly this pattern.
How to run this audit
Run the five checks above, in order, before every ship where you touched the database, storage, auth settings, or a function, not just before your first launch. A clean audit from last month tells you nothing about the table, bucket, or RPC your AI added this morning.
Want this checklist run for you, automatically?
Run a free, read-only scan of your live app: no install, results in under a minute.
Scan my app free →FAQ
How often should I run a Supabase security audit?
Before every deploy that touches your database schema, storage buckets, auth settings, or database functions, not just once before launch. Vibe-coded apps ship new tables and new RPCs constantly, and each one is a fresh chance for a missing policy or an over-permissioned function. Treat the audit as part of your ship checklist, the same way you'd treat running your tests.
Is a SECURITY DEFINER function always dangerous?
No. Plenty of legitimate use cases need one function to safely aggregate or touch data across tables that a normal user role shouldn't query directly. The risk is specific: a SECURITY DEFINER function that's also callable by anon or authenticated without its own internal checks. Audit what each one actually returns to an unauthenticated caller, and revoke execute access from roles that don't need it.
Do I need to audit auth settings if I never touch them after setup?
Yes, because "never touch them" is exactly how a leftover localhost redirect URL or a disabled email confirmation from early prototyping survives into production. These settings don't break your app if they're wrong, so nothing forces you to notice, which is why they belong in a periodic checklist instead of a one-time setup step.
The bottom line
A real supabase security audit is five checks, not one: RLS on every table, storage bucket exposure, which key lives where, sane auth configuration, and database functions that might bypass everything else. Skipping the last one is how a project with perfect RLS still leaks through a SECURITY DEFINER RPC nobody re-checked. An attacker can probe all five surfaces in minutes: the point is to run this checklist yourself first, and to keep running it, because every deploy your AI ships is a new table, bucket, or function that needs the same five checks.
Find your gaps before an attacker does.
Is My Site Hackable? scans your deployed app for the exact issues in this article (exposed keys, missing RLS, open buckets) and tells you what's real and what's a false alarm.
Run a free scan →