Before you push to production, run through this checklist. It's the same one I use on every audit, built from real findings in real apps. Each item takes under 60 seconds to check, and the whole thing fits in 15 minutes.
Most of these take one SQL query or one grep. None require external tools.
1. Committed Secrets (check before anything else)
The one that kills companies. A committed service-role key is game over — it bypasses all RLS.
# Run from your repo root
git log --all --full-history --diff-filter=A -- "*.env" ".env*"
grep -r "sb_secret_\|SUPABASE_SERVICE_ROLE" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.json" .
Fix if found: Rotate the key in Supabase Dashboard → Settings → API FIRST. Deleting the file does nothing — the key lives in git history forever. Then add .env* to .gitignore if it's not already there.
2. RLS Actually Enabled
SELECT c.relname, c.relrowsecurity
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public' AND c.relkind = 'r'
AND c.relrowsecurity = false;
Any row returned = a table with no row-level security. Everyone with the anon key can read/write it.
Fix: ALTER TABLE public.<table> ENABLE ROW LEVEL SECURITY;
3. No Permissive Policies
SELECT tablename, policyname, roles, cmd
FROM pg_policies
WHERE roles::text IN ('{anon}', '{public}')
AND qual = 'true';
Each row is a policy that says "anyone with the anon key can do this." Most of these are mistakes — login helpers that accidentally opened a whole table, or policies named "Users can read" that actually mean "anyone can read."
Fix: Add TO authenticated and a row predicate, or replace with a SECURITY DEFINER function for lookup use-cases.
4. No Write-All for Authenticated
SELECT tablename, policyname
FROM pg_policies
WHERE roles::text = '{authenticated}'
AND qual = 'true'
AND with_check = 'true'
AND cmd = 'ALL';
This means any logged-in user can write any row. Classic multi-tenant isolation failure.
Fix: Add row predicates: WITH CHECK (auth.uid() = user_id).
5. Service-Role Key Not in Client Code
# Check for client-side exposure
grep -r "SERVICE_ROLE" --include="*.tsx" --include="*.jsx" .
grep -r "VITE_.*SERVICE\|NEXT_PUBLIC_.*SERVICE" .
The VITE_ and NEXT_PUBLIC_ prefixes mean the variable is baked into the public JavaScript bundle. If any reference to the service-role key has these prefixes, the key is exposed.
Fix: Move the service-role key to server-side environment only. The client should never hold anything stronger than the anon key.
6. Storage Rules
SELECT id, name, public
FROM storage.buckets;
Any bucket with public = true is accessible without authentication. Make sure only genuinely public assets are in public buckets.
7. Auth Settings
Check in the Dashboard (Authentication → Providers):
- Email confirmations enabled
- Rate limiting not disabled
- Leaked password protection enabled
- No test users with admin privileges still active
8. October 30 Readiness
If your app creates tables via migrations, ensure each CREATE TABLE has a matching GRANT statement. See my full guide on the October 30 change.
The 60-Second Version
If you only do one thing from this checklist, do #1 (committed secrets). It's the only one that can't be fixed after the fact.
If you have two minutes, also run #2 and #3 — RLS disabled and permissive policies are the two findings I see in almost every audit.
Automate This
I built a free client-side scanner that runs checks #1, #2, and #3 on any public GitHub repo — entirely in your browser, nothing stored: rls.cenkkurtoglu.com
If you'd rather have a human run the full list and hand you a severity-ranked report with fix SQL — that's the service I offer. Spot Check is $99, Full Audit is $199, both delivered in 48 hours.
Every pattern in this checklist comes from real production audits. No hypotheticals. If you found an issue in your own app from this list — fix the key rotation first, then the RLS, then the policies. That's the priority order that minimizes damage.
Top comments (0)