1. Row Level Security (RLS)
Row Level Security is your primary defense mechanism in Supabase. It ensures that even if someone gets your anon key (which is designed to be public), they can only access data they're authorized to see.
Enable RLS on Every Table
The first rule is simple: enable RLS on every table in your public schema. Tables without RLS are accessible to anyone with your project URL and anon key.
-- Enable RLS on a table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Verify RLS is enabled
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';
A table with RLS enabled but no policies blocks ALL access (except for service_role). Always add at least one policy after enabling RLS.
Common RLS Patterns
Here are the most common patterns you'll use:
User owns row:
CREATE POLICY "Users can access own data"
ON user_profiles FOR ALL
USING (user_id = auth.uid());
Public read, private write:
-- Anyone can read
CREATE POLICY "Public read" ON posts FOR SELECT USING (true);
-- Only owner can modify
CREATE POLICY "Owner write" ON posts FOR INSERT
WITH CHECK (user_id = auth.uid());
The WITH CHECK Clause
For INSERT and UPDATE operations, use WITH CHECK to validate the data being written. This prevents users from inserting rows they shouldn't own:
CREATE POLICY "Insert own posts" ON posts FOR INSERT
WITH CHECK (user_id = auth.uid());
CREATE POLICY "Update own posts" ON posts FOR UPDATE
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
2. API Key Management
Supabase gives you two main API keys: anon and service_role. Understanding the difference is crucial for security.
Anon Key (Public)
- Designed to be used in client-side code (browsers, mobile apps)
- Safe to expose - your security relies on RLS policies
- Has the
anonrole in PostgreSQL - Cannot bypass RLS policies
Service Role Key (Secret)
- Bypasses ALL Row Level Security policies
- Should NEVER be exposed in client-side code
- Use only in secure server environments
- Equivalent to database superuser access
Never put your service_role key in client-side JavaScript, React components, or anywhere it could be extracted from your app bundle.
Key Storage Best Practices
# .env.local (client-side - safe)
NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
# .env (server-side only - keep secret)
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
3. Authentication Security
Enable Email Confirmation
Prevent fake accounts by requiring email confirmation before users can access your app:
- Go to Authentication → Settings in your Supabase dashboard
- Enable "Confirm email"
- Configure your email templates
Password Requirements
Set minimum password strength requirements to prevent weak passwords. Supabase supports configuring minimum length and complexity rules.
Configure Redirect URLs
Whitelist only your domains in the redirect URL settings. This prevents open redirect vulnerabilities where attackers could redirect users to malicious sites.
// Allowed redirect URLs (configure in dashboard)
https://yourapp.com/*
https://www.yourapp.com/*
http://localhost:3000/* // For development only
Rate Limiting
Supabase has built-in rate limiting for auth endpoints. Review the default limits and adjust if needed:
- Signup: 30 requests per hour per IP
- Token refresh: 360 requests per hour per user
- Password reset: 30 requests per hour per email
4. Storage Security
Supabase Storage has its own RLS policies separate from database tables.
Enable RLS on Buckets
-- Example: Users can only access their own files
CREATE POLICY "User folder access"
ON storage.objects FOR ALL
USING (bucket_id = 'avatars' AND auth.uid()::text = (storage.foldername(name))[1]);
Validate File Types
Restrict uploads to specific file types to prevent malicious file uploads:
CREATE POLICY "Images only"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'images' AND
(storage.extension(name) = 'jpg' OR
storage.extension(name) = 'png' OR
storage.extension(name) = 'webp')
);
5. Edge Functions & Database Functions
Edge Functions Security
Always validate the JWT token in your Edge Functions before performing sensitive operations:
import { createClient } from '@supabase/supabase-js'
Deno.serve(async (req) => {
const authHeader = req.headers.get('Authorization')
if (!authHeader) {
return new Response('Unauthorized', { status: 401 })
}
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{ global: { headers: { Authorization: authHeader } } }
)
const { data: { user }, error } = await supabase.auth.getUser()
if (error || !user) {
return new Response('Unauthorized', { status: 401 })
}
// User is authenticated, proceed...
})
Database Functions
Be careful with SECURITY DEFINER functions - they run with elevated privileges:
-- SECURITY INVOKER (default, recommended)
-- Runs with caller's permissions
CREATE FUNCTION get_user_data()
RETURNS TABLE (...)
SECURITY INVOKER
AS $$
SELECT * FROM users WHERE id = auth.uid();
$$;
-- SECURITY DEFINER (use carefully)
-- Runs with function owner's permissions
CREATE FUNCTION admin_get_all_users()
RETURNS TABLE (...)
SECURITY DEFINER
AS $$
-- Add your own permission check!
IF NOT is_admin(auth.uid()) THEN
RAISE EXCEPTION 'Not authorized';
END IF;
SELECT * FROM users;
$$;
6. Common Security Mistakes
Mistake 1: Forgetting RLS on New Tables
Every time you create a table, immediately enable RLS. Make it a habit.
Mistake 2: Overly Permissive Policies
-- BAD: Anyone can do anything
CREATE POLICY "open_access" ON users FOR ALL USING (true);
-- GOOD: Users can only access their own data
CREATE POLICY "user_access" ON users FOR ALL USING (id = auth.uid());
Mistake 3: Exposing Service Role Key
Never use environment variables prefixed with NEXT_PUBLIC_ or VITE_ for your service role key.
Mistake 4: Not Testing Policies
Test your RLS policies by impersonating different users in the SQL editor. Use the role switcher to test as anon, authenticated, or specific users.
Mistake 5: Trusting Client Data
Always validate data on the server. RLS policies should not rely on client-provided user IDs - always use auth.uid().
7. Security Checklist
Use our interactive security checklist to track your progress with saved state.
- ☐ RLS enabled on all public tables
- ☐ At least one policy per table
- ☐ No service_role key in client code
- ☐ Email confirmation enabled
- ☐ Redirect URLs whitelisted
- ☐ Storage buckets have RLS policies
- ☐ Edge Functions validate auth
- ☐ Database functions use SECURITY INVOKER
- ☐ Tested policies as different user roles
- ☐ Regular security audits scheduled
Automate Your Security Audits
SupaExplorer can scan your Supabase project automatically and generate AI-powered fix recommendations.
Run Free Audit