Supabase Security Checklist: 15 Things to Check Before Launch
Supabase exposes your Postgres database to the browser. That's powerful — and dangerous if RLS is off. Here are 15 concrete checks, with real policies, before you go live.

The Supabase trade-off
Supabase hands the browser a direct line to your Postgres database using the public anon key. That's the whole point — and the whole risk. Without Row Level Security (RLS), anyone with your anon key (which ships in the client bundle) can read and write your tables. Here are 15 checks before launch.
1. RLS is enabled on every table
This is the big one. A table without RLS is fully readable/writable via the anon key.
alter table public.profiles enable row level security;
alter table public.orders enable row level security;
Verify there are no exceptions:
select tablename, rowsecurity
from pg_tables
where schemaname = 'public' and rowsecurity = false;
That query should return zero rows for any table holding real data.
2. RLS is enabled AND policies exist
RLS with no policies denies everything (safe but broken). RLS off allows everything (dangerous). You want RLS on with correct policies.
create policy "users read own profile"
on public.profiles for select
using (auth.uid() = user_id);
3. Separate policies per operation
Don't use one for all policy. Be explicit about select/insert/update/delete:
create policy "insert own row" on public.orders
for insert with check (auth.uid() = user_id);
create policy "update own row" on public.orders
for update using (auth.uid() = user_id)
with check (auth.uid() = user_id);
using filters which rows are visible; with check validates rows being written. You usually need both.
4. The service_role key is server-only
The service_role key bypasses RLS entirely. If it ever reaches the browser, your database is wide open. It belongs only in server environments:
grep -rn "service_role\|SERVICE_ROLE" ./app ./components ./src
Any hit in client-side code is a critical finding.
5. Only the anon key is public
Confirm your client uses NEXT_PUBLIC_SUPABASE_ANON_KEY — never the service key. The anon key is meant to be public; it's safe only because RLS is on.
6. Storage buckets have policies
Storage is governed by its own RLS-style policies. A public bucket is readable by the entire internet.
create policy "users read own files"
on storage.objects for select
using (bucket_id = 'avatars' and auth.uid()::text = (storage.foldername(name))[1]);
7. No privilege fields the user can write
Never let a user update their own role or is_admin. Restrict which columns the policy permits, or enforce it with a trigger / separate admin path.
8. Database functions use the right security context
security definer functions run with the creator's privileges and bypass the caller's RLS. Use them deliberately, set a safe search_path, and prefer security invoker unless you specifically need elevation:
create function public.get_my_orders()
returns setof orders
language sql
security invoker
set search_path = public
as $$ select * from orders where user_id = auth.uid(); $$;
9. The anon role can't reach sensitive schemas
Lock down direct grants. The anon and authenticated roles should only touch what they need.
10. Email confirmations are on
In Auth settings, require email confirmation so attackers can't sign up as arbitrary addresses and inherit access.
11. Strong password and rate-limit settings
Set a minimum password length and enable Supabase's built-in auth rate limiting to blunt brute-force and enumeration.
12. Realtime is scoped by RLS
Realtime subscriptions respect RLS only if it's enabled. A table broadcasting changes without RLS leaks every row to every subscriber.
13. No secrets in Edge Functions logs
Edge Functions are great, but console.log of a token or key ends up in logs. Scrub them.
14. CORS and allowed redirect URLs are tight
In Auth → URL Configuration, list only your real domains as redirect URLs. A wildcard here enables token-stealing redirects.
15. Test as an unauthenticated user
The real proof. Use the anon key with no session and try to read a protected table:
const supabase = createClient(url, ANON_KEY);
const { data, error } = await supabase.from("orders").select("*");
// data should be [] (or error) — NOT every order in the table
If you get rows back, RLS is misconfigured. Fix it before launch.
Checklist
- RLS enabled on every table
- Correct per-operation policies (using + with check)
- service_role key server-only
- Storage bucket policies set
- No client-writable privilege fields
- security definer functions audited
- Realtime tables RLS-protected
- Redirect URLs and CORS locked down
- Verified as an anonymous user
Turn the checklist into an authorization test matrix
RLS is not complete when the toggle is on; it is complete when every role, operation and ownership boundary has a passing negative test. Build a matrix with anonymous, user A, user B and the service role across select, insert, update and delete. For each table, assert what should succeed and—more importantly—what must return no rows or an authorization error.
Use a disposable staging project with synthetic records. Create two users, seed one row for each and call the same Supabase client methods the browser uses. Test guessed UUIDs, filters, joins, storage paths and Realtime subscriptions. A policy that protects select can still permit an unsafe update; a correct table policy can be bypassed by an overly privileged security-definer function.
Bound the public API deliberately
Supabase's API security documentation recommends RLS as a core control and also describes extra defenses such as exposing a custom schema instead of leaving every object in public reachable through the data API. Revoke unnecessary table and function grants. Audit views and functions for execution context, fix search_path on privileged functions, and keep secrets in a server-side secret store rather than table rows readable by application roles.
The anon or publishable key is not a secret; its safety depends on policies. The service-role key is privileged and must remain in trusted server code. Never move it into a NEXT_PUBLIC_ variable to solve a browser error. If that happened, rotate it and inspect access logs instead of merely renaming the variable.
Capture launch evidence
For each critical table and bucket, retain the policy definition, the negative test, the date and the migration that introduced it. Enable suitable database backups and point-in-time recovery for the business risk, review SSL enforcement and network restrictions, and route security-relevant auth and database events to an alerting destination. Supabase's production checklist also calls out email deliverability, rate limits and ownership of operational credentials—security includes the ability to recover safely.
Re-run the matrix in CI after database migrations. A lightweight integration suite catches policy regressions that a static SQL review misses. Combine it with the Next.js and Supabase end-to-end checklist and the Next.js security guide for the server boundary.
Scan it with Troja
Troja tests your Supabase project the way an attacker would — using your public anon key to probe which tables and buckets actually return data — and flags every table where RLS isn't doing its job. Scan it and see exactly what's reachable.
Frequently asked questions
Is enabling RLS enough to secure a Supabase table?
No. You need operation-specific policies and negative tests for anonymous users, multiple authenticated owners and any privileged path. RLS with no policy denies access; a broad policy can expose every row.
Is the Supabase anon or publishable key safe in the browser?
It is designed to be public, but only within the permissions enforced by RLS and database grants. Treat a policy failure—not visibility of that public key—as the primary risk.
What should happen if a service-role key reached client code?
Rotate the key, remove it from client-visible variables and bundles, review logs for misuse, and add a server-only boundary plus a build or bundle check to prevent recurrence.
How do I prevent RLS regressions after launch?
Run a staging authorization matrix after every database migration. Include allowed and denied select, insert, update and delete cases for anonymous users and at least two record owners.
Sources and verification notes
Product capabilities are vendor-attributed and source-dated. Technical guidance uses primary documentation or vendor-neutral standards.
- Supabase production checklistPrimary launch guidance for security, availability, rate limits and operational readiness.
- Supabase secure configurationPrimary source for platform security configuration and shared responsibilities.
- Supabase Row Level SecurityPrimary source for enabling RLS and writing policies with auth context.
- Securing the Supabase Data APIPrimary source for API exposure, schemas, grants and defense-in-depth controls.
Run the scan this post is about.
Free, no signup. See what's hiding inside your walls in ~30 seconds.
Keep reading
All posts
How to Secure Your Next.js + Supabase App: A Complete Security Checklist
The Next.js + Supabase stack is fast to ship and easy to leak. This end-to-end checklist covers RLS, the anon vs service key, auth, headers, and the bundle — with real code.
Read
Next.js Security Best Practices: 10 Things Most Developers Miss
Next.js is secure by default — until you reach for client components, route handlers, and middleware. Here are ten places the framework's footguns hide, with the correct patterns.
Read
SaaS Security Checklist Before Launch: The MVP Guide
Shipping your MVP this week? Run this pragmatic, prioritized security pass first — covering auth, multi-tenancy, secrets, payments, and the few headers that actually matter.
Read