Most Supabase apps go live with at least one critical misconfiguration. This checklist covers RLS policies, exposed anon keys, Auth hardening, Storage rules, and what to scan before your first user signs up.
Anay Pandya
838 words
Anay Pandya
Founder of ZeriFlow · 10 years fullstack engineering · About the author
Key Takeaways
Most Supabase apps that go live have at least one critical misconfiguration. Not because developers are careless — because Supabase is incredibly fast to ship with, and security configuration is a separate layer that the platform does not enforce by default. This checklist covers the six areas that cause the most incidents: Row Level Security, anon key exposure, Auth configuration, Storage rules, Edge Functions, and security headers.
If you check all six before your first real user signs up, you will have eliminated the majority of attack surface that vibe-coded Supabase apps leave open.
Is your site actually secure?
Run a free check — 60 seconds
Row Level Security is the most important security feature in Supabase and the most frequently misconfigured one.
Keep reading
What goes wrong: During development, developers often enable RLS and then add a permissive policy so they can work without interruptions. The classic mistake:
-- WRONG: This allows anyone to read everything
CREATE POLICY "allow all" ON public.profiles
FOR SELECT USING (true);This policy satisfies the "RLS is enabled" requirement while providing zero protection. Anyone who knows your Supabase URL can query every row in the table.
What correct looks like:
-- CORRECT: Users can only see their own profile
CREATE POLICY "users can view own profile" ON public.profiles
FOR SELECT USING (auth.uid() = user_id);
-- CORRECT: Users can only update their own profile
CREATE POLICY "users can update own profile" ON public.profiles
FOR UPDATE USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);Before launch checklist for RLS:
- Every table with user data has RLS enabled
- No policy uses USING (true) on a table with sensitive data
- Insert policies use WITH CHECK to validate ownership
- You have tested policies as an authenticated user AND as an anonymous user
- You have tested cross-user isolation (user A cannot read user B data)
To audit your policies, run this in the Supabase SQL editor:
SELECT schemaname, tablename, policyname, cmd, qual
FROM pg_policies
WHERE schemaname = 'public'
ORDER BY tablename;The Supabase anon key is designed to be public — it is what your frontend uses to interact with Supabase from the browser.
What you must never do: Use the service_role key on the client side. The service_role key bypasses all RLS policies.
// WRONG: Never use service_role in client code
const supabase = createClient(url, process.env.SUPABASE_SERVICE_ROLE_KEY)
// CORRECT: Use anon key on client, service_role only in server-side code
const supabase = createClient(url, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)In Settings > API, restrict the anon key to your production domain before launch.
Email enumeration protection: Enable this in Authentication > Settings. Without it, different responses for "email not found" vs "wrong password" let attackers probe which email addresses have accounts.
Redirect URL whitelist: Set your Site URL and add only your production domains. Remove default wildcard entries.
Signup control: If your app is invitation-only, disable public signup in Authentication > Settings.
Rate limiting: Tighten limits on /auth/v1/token and /auth/v1/signup to stop credential stuffing attacks.
Use public buckets only for genuinely public assets. All user-uploaded content should be in private buckets with RLS policies on storage.objects:
CREATE POLICY "users can upload own files" ON storage.objects
FOR INSERT WITH CHECK (
bucket_id = 'user-uploads' AND
auth.uid()::text = (storage.foldername(name))[1]
);For sensitive files, generate signed URLs server-side:
const { data } = await supabase.storage
.from('private-docs')
.createSignedUrl(filePath, 3600) // expires in 1 hourAlways validate the JWT in edge functions:
const authHeader = req.headers.get('Authorization')
if (!authHeader) return new Response('Unauthorized', { status: 401 })
const { data: { user } } = await supabaseClient.auth.getUser()
if (!user) return new Response('Unauthorized', { status: 401 })Never hardcode the service_role key in function code. Do not use Access-Control-Allow-Origin: * in functions that handle authenticated operations.
Supabase does not add security headers to your frontend. Configure them in your Next.js deployment:
// next.config.js
const securityHeaders = [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
]
module.exports = {
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }]
}
}USING (true) policies on sensitive tablesScan your deployed app at zeriflow.com/free-scan to catch security header issues, exposed endpoints, and TLS misconfigurations automatically. The scan takes 60 seconds.
Related resources
Website Vulnerability Scanner
Run a broader website security audit across headers, TLS, DNS, cookies, SEO, and disclosure checks.
Security Headers Checker
Check CSP, HSTS, X-Frame-Options, and other response headers.
SSL Checker
Review TLS certificate, HTTPS, and transport security signals.
DMARC Checker
Validate email authentication records for domain spoofing protection.
CSP Checker
Review Content-Security-Policy coverage and common gaps.
May 10, 2026
How to Secure Your AI-Generated Next.js App (2026 Guide)
Monitor your security score
Schedule recurring scans and catch regressions after deployments.