Supabase Security Best Practices: RLS, Keys, and CORS
Supabase security hinges on a single architectural reality: your frontend app talks directly to a PostgreSQL database over a REST/GraphQL API. There's no traditional backend server in the request path to validate permissions. That job falls entirely to Row Level Security policies and correct API key management. Get those wrong and every row in your database is potentially exposed.
Check your site's security right now: Free ZeriFlow scan →
1. Row Level Security: The Single Most Important Control
Row Level Security (RLS) is PostgreSQL's built-in mechanism for restricting which rows a database user can access. In Supabase, the anon and authenticated roles (corresponding to your API keys) are PostgreSQL roles — and RLS policies determine what data those roles can read or write.
The critical default: When you create a new table in Supabase, RLS is disabled by default. Every row is accessible to anyone with your anon key (which is publicly visible in your client-side code).
Enable RLS immediately on every table:
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;
-- Repeat for every tableOnce RLS is enabled with no policies defined, the table becomes completely inaccessible (deny-all). Then build up policies:
-- Users can read their own profile
CREATE POLICY "Users read own profile"
ON public.profiles FOR SELECT
USING (auth.uid() = user_id);
-- Users can update their own profile
CREATE POLICY "Users update own profile"
ON public.profiles FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- Admins can read all profiles
CREATE POLICY "Admins read all profiles"
ON public.profiles FOR SELECT
USING (auth.jwt() ->> 'role' = 'admin');Use Supabase's RLS Policy Editor in the dashboard, or manage policies in migration files for version control.
2. API Key Hygiene: Anon vs Service Role
Supabase provides two primary API keys, and confusing their use is a critical security mistake.
`anon` key:
- Designed to be included in client-side (browser/mobile) code.
- Operates under the anon PostgreSQL role.
- RLS policies apply — users are restricted by what policies allow.
- Safe to expose (with proper RLS policies in place).
`service_role` key: - Bypasses ALL RLS policies. Any query made with this key has full unrestricted access to your entire database. - Must never appear in client-side code, frontend repositories, or anywhere that gets bundled into a browser-deliverable artifact. - Use only in server-side environments: backend API routes, Edge Functions, CI/CD scripts.
Detection: If you've accidentally committed a service_role key to a public GitHub repo, rotate it immediately in the Supabase dashboard. Treat it as compromised.
Run a free ZeriFlow scan → on your Supabase-powered app's public URL to check whether security headers are properly set and your API endpoint doesn't leak unnecessary information in response headers.
3. CORS Configuration for Your Supabase Project
Supabase's API endpoints are accessible from any origin by default. While RLS protects the data layer, restricting CORS to known origins provides an additional defense-in-depth layer.
Configure allowed origins in the Supabase dashboard:
Navigate to Project Settings → API → CORS Allowed Origins. Add only the origins that need access:
https://yourdomain.com
https://www.yourdomain.com
https://app.yourdomain.comAvoid * (wildcard) in production. A wildcard CORS policy combined with a weak RLS policy creates a wide attack surface.
For local development: Add http://localhost:3000 (or your local dev port) during development, but ensure production origins are locked down. Use environment variables to switch CORS origins between dev and prod.
Edge Functions CORS: If you're using Supabase Edge Functions as an API layer, handle CORS in the function itself:
const corsHeaders = {
'Access-Control-Allow-Origin': 'https://yourdomain.com',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};4. Supabase Auth Security
Supabase Auth (GoTrue) handles email/password, magic links, OAuth, and phone authentication. Several configuration settings materially affect security.
Email confirmation:
Enable email confirmation in Authentication → Providers → Email → Confirm email. Without it, users can sign up with any email address — including addresses they don't own — and immediately access your application.
JWT expiry:
The default JWT expiry is 3600 seconds (1 hour). The refresh token is valid for 7 days. Assess whether these durations match your application's risk profile. Financial applications may want shorter-lived sessions.
Password policies:
Set a minimum password length (at least 12 characters) and optionally enable "Prevent use of leaked passwords" (checks against the HaveIBeenPwned database) in Authentication → Providers → Email → Password security.
OAuth provider hygiene:
Disable OAuth providers you don't actively use. Each enabled provider is an additional login vector. Review which redirect URIs are allowed for each provider and remove development URLs before launch.
Session invalidation:
Supabase provides supabase.auth.admin.signOut(userId) via the Admin API (server-side only, service role key required) to forcibly sign out a user — useful for account compromise response.
5. Auth Rate Limiting
Brute-force attacks against login and sign-up endpoints are common. Supabase has built-in rate limiting for authentication endpoints, configurable in the dashboard.
Key rate limits to configure (Authentication → Rate Limits):
- Sign-up rate: Limit sign-ups per IP per hour to prevent account creation abuse.
- Sign-in rate: Limit failed sign-in attempts. The default is permissive.
- OTP / Magic link rate: Prevent magic link spam (email bombing).
- Token refresh rate: Limit token refresh attempts.
At the infrastructure level, consider placing Cloudflare in front of your Supabase project's domain. Cloudflare's WAF and bot management add a layer of rate limiting and challenge pages that sit before requests even reach Supabase.
6. Securing the Supabase Dashboard and Project Access
The Supabase dashboard itself (app.supabase.com) is a high-value target — access to it means access to your database schema, API keys, and production data.
Controls:
- Enable MFA on your Supabase account (account settings). This is a single point of failure if neglected.
- Use team member roles when collaborating. Supabase supports owner, administrator, and developer roles with different permission levels. Don't share owner credentials.
- Rotate your API keys if any team member with key access departs.
- Restrict database connections (direct Postgres connections via the connection string) to known IP addresses using the connection pooler's trusted IP allow list.
- Never expose your direct Postgres connection string to clients. Use the Supabase REST API or Edge Functions as the access layer.
FAQ
### Q: Is it safe to use the Supabase anon key in a React app? A: Yes — if your RLS policies are correctly configured. The anon key is designed for public client use. The protection model relies on RLS, not key secrecy. Without proper RLS, the anon key gives access to all data. With proper RLS, it can only access what you've explicitly allowed.
### Q: What happens if I enable RLS but don't add any policies?
A: The table becomes fully inaccessible to the anon and authenticated roles — all queries return empty results. This is the secure default. Add policies to explicitly grant required access.
### Q: How do I check if my Supabase project's API is leaking information in response headers? A: Run a ZeriFlow scan on your Supabase project URL or your app's public-facing URL. ZeriFlow checks response headers, cookie flags, TLS configuration, and DNS records.
### Q: Can I use Supabase without exposing the database schema to the browser? A: Supabase exposes your schema structure through PostgREST introspection (accessible via the anon key). You can disable schema introspection in project settings if you don't want schema details visible to unauthenticated requests.
### Q: How should I handle the service_role key in a Next.js app?
A: The service_role key must only be used in server-side code: API routes (/api/*), server actions, or middleware — code that runs on the server and is never included in the client bundle. Store it in an environment variable that does NOT have a NEXT_PUBLIC_ prefix (which would expose it to the browser).
Conclusion
Supabase's architecture — direct database access from the client — is powerful but requires security-first thinking from day one. RLS is not optional: it's the mechanism that makes Supabase safe to use in frontend applications. Layer on top of that correct API key hygiene, restrictive CORS, auth hardening, and rate limiting, and you have a production-grade security posture.
Validate your application's external security footprint — headers, TLS, and cookie configuration — with an automated scan.
Run a free ZeriFlow scan → — 60 seconds, no credit card.