Supabase Security Hardening Checklist
Security First -- Part 27 of 30
When the Database Is the Front Door
Marcus had been building for three weeks. His AI-assisted project management app had real users, real data, and -- he thought -- real security. He had set up Supabase, wired up authentication, and deployed to Vercel. The anon key was in his frontend, which he had read was fine.
What Marcus had not done was enable Row Level Security on his tasks table.
On a Tuesday morning, a security researcher sent him a polite but alarming email. By hitting https://his-project.supabase.co/rest/v1/tasks?select=*, the researcher had pulled every task from every user in the database -- names, emails embedded in task descriptions, project details, client notes. No authentication required. One URL, thousands of records, zero friction.
Marcus is not alone. In September 2025, researchers at DeepStrike documented that hundreds to thousands of Supabase-backed applications were exposed this way -- including startups, medium-sized businesses, and large enterprises. The attack required no special skill: just knowledge of the predictable /rest/v1/<table> pattern that Supabase auto-generates.
A separate February 2026 case study found that a single misconfigured anon key led to the exposure of 28,000 customer records, Server-Side Request Forgery vulnerabilities, and complete account takeover capabilities. The root cause in every case: a misunderstanding of who is responsible for access control in the Supabase model.
That responsibility is yours. This checklist is how you meet it.
Why Supabase Security Is Different
In a traditional backend, your Express or Django server is the gatekeeper. Users never talk directly to your database. In Supabase, the database is the API. PostgREST generates REST endpoints directly from your PostgreSQL tables. That means your database's own access controls -- specifically Row Level Security -- are your backend security layer.
This is powerful when configured correctly. It is catastrophic when it is not.
The Supabase Security Hardening Checklist
Work through each item below. Most can be completed in under 15 minutes each.
1. Enable RLS on Every Table -- No Exceptions
By default, tables created via the SQL editor have RLS disabled. That means any request using your anon key can read every row.
-- Enable RLS on a table
ALTER TABLE public.tasks ENABLE ROW LEVEL SECURITY;
-- Add a policy so users only see their own rows
CREATE POLICY "Users can view own tasks"
ON public.tasks FOR SELECT
USING (user_id = auth.uid());
-- Also protect writes
CREATE POLICY "Users can insert own tasks"
ON public.tasks FOR INSERT
WITH CHECK (user_id = auth.uid());
Do this for every table. Use the Supabase Security Advisor (Dashboard > Reports > Security) to find tables with RLS disabled -- it runs check 0013 specifically for this.
2. Never Expose Your Service Role Key
Your project has two keys: the anon key and the service_role key. The service role key bypasses all RLS policies entirely -- it is a master credential with full read/write access to your entire database.
It should only ever live in server-side environment variables (a Vercel server function, an edge function, a backend API). If it appears in your frontend JavaScript bundle, your application is fully compromised.
Search your codebase:
grep -r "service_role" ./src
If you find it anywhere in client-side code, rotate it immediately in your Supabase dashboard and move it to a secret.
3. Understand What the Anon Key Actually Is
The anon key is safe to expose client-side -- but only when RLS is properly configured. It is the public key that unauthenticated visitors use to interact with your API. If your RLS policies are correct, the anon key can only access what you explicitly allow. If RLS is disabled or overly permissive, the anon key is a skeleton key to your data.
The anon key being public is not the vulnerability. Missing RLS is.
4. Harden Auth Configuration
In your Supabase Dashboard under Authentication > Providers > Email:
- Enable email confirmation. Without it, an attacker can register with a fake email address and receive a valid JWT that satisfies
auth.role() = 'authenticated'policies -- effectively forging an authenticated session. A March 2026 analysis on DEV Community called this the "Ghost Auth" flaw. - Set minimum password length to at least 8 characters (12 is better).
- Enable rate limiting on auth endpoints. Supabase includes basic rate limiting by default, but review your thresholds under Authentication > Rate Limits.
5. Lock Down Storage Buckets
Storage buckets created as "public" serve files to anyone with the URL -- no authentication, no authorization. Audit your buckets under Storage in the dashboard.
For user-uploaded files, keep buckets private and generate signed URLs with expiration times:
-- Storage policy: users can only access their own files
CREATE POLICY "User file access"
ON storage.objects FOR SELECT
USING (auth.uid()::text = (storage.foldername(name))[1]);
6. Use Supabase Vault for Secrets in Edge Functions
If your Edge Functions connect to third-party APIs (Stripe, Resend, OpenAI), those credentials must not be hardcoded. Supabase Vault stores secrets encrypted at rest.
# Store a secret via the CLI
supabase secrets set STRIPE_SECRET_KEY=sk_live_...
In your edge function, access it via Deno.env.get('STRIPE_SECRET_KEY'). Never paste API keys directly into function code.
7. Apply Least Privilege to Database Roles
The anon and authenticated roles should only have access to what they genuinely need. Avoid blanket GRANT ALL statements. If you use the http extension for outbound requests, be especially careful: a September 2025 pentest finding showed that giving anon or authenticated roles EXECUTE on network-capable functions allowed Server-Side Request Forgery via /rest/v1/rpc/http_get.
-- Revoke unnecessary permissions
REVOKE EXECUTE ON FUNCTION public.http_get FROM anon, authenticated;
8. Restrict Direct Database Access
Your Supabase database has a direct connection string (port 5432) and a pooled connection via Supavisor (port 6543). For application code, always use Supavisor. For direct access, enable IP allowlisting in your project settings under Database > Network Restrictions. If your production server has a static IP, allowlist only that address. This prevents opportunistic port scanning from reaching your database.
9. RLS Applies to Realtime Subscriptions Too
If you use Supabase Realtime to listen for database changes, your RLS policies govern what rows the subscription can see. But this only works if you have RLS enabled and policies defined. A subscription to a table without RLS will stream all changes to all subscribers -- including changes to other users' rows.
Test your Realtime subscriptions the same way you test your REST endpoints: as a different user, verify you cannot see their data updates.
10. Enable Audit Logging
For applications handling personal data, financial records, or anything compliance-relevant, enable pgAudit to log who ran what queries. This is available on Pro and Enterprise plans. At minimum, review the logs available in your Supabase Dashboard under Logs > Database -- they capture slow queries, errors, and connection patterns.
11. Verify Your Backup Restore Process
As covered in Day 23, a backup you have never tested is not a backup. Supabase Pro includes daily backups with point-in-time recovery. Confirm your backup retention period in Database > Backups, and actually restore a backup to a staging project at least once to verify the process works before you need it.
12. Enable Two-Factor Authentication on Your Supabase Dashboard Account
Your Supabase dashboard has full access to your project settings, API keys, database, and secrets. If that account is compromised, everything else on this list becomes irrelevant. Enable 2FA under your account settings (avatar > Account > Security). Use an authenticator app, not SMS.
The Master Checklist
Copy this and mark each item complete:
- RLS enabled on every table in the public schema
- RLS policies use
auth.uid()-- never trust IDs from request body - Service role key is server-side only, not in frontend code
- Email confirmation enabled in Auth settings
- Password minimum length configured
- Rate limiting reviewed on auth endpoints
- Storage buckets are private; signed URLs used for file access
- Edge function secrets stored in Vault, not hardcoded
-
anonandauthenticatedroles audited for least privilege - Network restrictions configured; direct DB access restricted
- Realtime subscriptions tested as a non-privileged user
- Audit logging enabled or dashboard logs reviewed regularly
- Backup restore process tested at least once
- Two-factor authentication enabled on your Supabase account
- Supabase Security Advisor run with zero critical findings
Ask The Guild
Share your experience in the community thread:
"Which item on this checklist surprised you most? Did the Security Advisor flag anything in your project that you had not noticed? Drop your findings -- and your RLS policy patterns -- in the comments."
The most common gap we see in vibe-coder projects is not the service role key. It is the table that got created three weeks into the project, when the developer was in a hurry, that never got RLS enabled. Run the Security Advisor. Find that table before someone else does.