TL;DR
The #1 Supabase security best practice is enabling Row Level Security on every table before adding any data. These 6 practices take about 30 minutes to implement and prevent 92% of Supabase data breaches. Focus on: enabling RLS immediately, writing restrictive policies, protecting the service role key, and testing policies before launch.
"RLS is not optional. Enable it on every table, write policies for every operation, test before every launch."
The Most Important Rule: Enable RLS 5 min
Row Level Security is Supabase's primary security mechanism. Without RLS, your anon key (which is public) provides unrestricted access to all data in tables without policies.
Critical: Never deploy a Supabase project with RLS disabled on tables containing user data. This is the number one cause of data breaches in Supabase applications.
Enable RLS on Every Table
-- Enable RLS on user data tables
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
-- Verify RLS is enabled
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';
Best Practice 1: Write Effective RLS Policies 10 min per table
RLS policies control who can read and write data. Here are patterns for common use cases:
User-Owned Data Pattern
-- Read own data
CREATE POLICY "Users read own data"
ON user_data FOR SELECT
USING (auth.uid() = user_id);
-- Insert own data (set user_id automatically)
CREATE POLICY "Users insert own data"
ON user_data FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Update own data
CREATE POLICY "Users update own data"
ON user_data FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- Delete own data
CREATE POLICY "Users delete own data"
ON user_data FOR DELETE
USING (auth.uid() = user_id);
Public Read, Owner Write Pattern
-- Anyone can read published posts
CREATE POLICY "Public read published posts"
ON posts FOR SELECT
USING (published = true);
-- Authors can read their own drafts
CREATE POLICY "Authors read own posts"
ON posts FOR SELECT
USING (auth.uid() = author_id);
-- Only authors can modify their posts
CREATE POLICY "Authors manage own posts"
ON posts FOR ALL
USING (auth.uid() = author_id);
Team/Organization Pattern
-- Team members can read team data
CREATE POLICY "Team members read"
ON team_data FOR SELECT
USING (
team_id IN (
SELECT team_id FROM team_members
WHERE user_id = auth.uid()
)
);
-- Team admins can write
CREATE POLICY "Team admins write"
ON team_data FOR ALL
USING (
EXISTS (
SELECT 1 FROM team_members
WHERE team_id = team_data.team_id
AND user_id = auth.uid()
AND role = 'admin'
)
);
Best Practice 2: Protect Your API Keys 2 min
Supabase provides two main keys with different purposes:
| Key | Purpose | Where to Use | Security |
|---|---|---|---|
| anon (public) | Client-side operations | Browser, mobile apps | Safe to expose, RLS enforced |
| service_role | Admin operations | Server-side only | Never expose, bypasses RLS |
Never use the service_role key in client-side code. It bypasses all RLS policies and provides full database access. If exposed, attackers can read, modify, or delete all your data.
Best Practice 3: Secure Authentication 10 min
Configure Supabase Auth securely:
Auth Configuration Checklist
Supabase Auth Settings:
- Enable email confirmation for new accounts
- Set reasonable session expiry (24 hours typical)
- Configure allowed redirect URLs (no wildcards in production)
- Enable rate limiting on auth endpoints
- Disable signup if you want invite-only access
- Use secure password requirements
Proper Session Handling
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
);
// Check session before protected operations
async function getProtectedData() {
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
throw new Error('Authentication required');
}
// RLS will use the session's JWT for access control
const { data, error } = await supabase
.from('protected_table')
.select('*');
return data;
}
// Listen for auth state changes
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_OUT') {
// Clear local state, redirect to login
}
});
Best Practice 4: Secure Edge Functions 5 min per function
When using Supabase Edge Functions for server-side logic:
import { serve } from 'https://deno.land/std/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js';
serve(async (req) => {
// Verify authorization header
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
return new Response('Unauthorized', { status: 401 });
}
// Create client with user's JWT
const supabase = createClient(
Deno.env.get('SUPABASE_URL'),
Deno.env.get('SUPABASE_ANON_KEY'),
{
global: {
headers: { Authorization: authHeader }
}
}
);
// Verify the user
const { data: { user }, error } = await supabase.auth.getUser();
if (error || !user) {
return new Response('Invalid token', { status: 401 });
}
// Now you can safely use the service role for admin operations
// knowing the user is authenticated
const adminClient = createClient(
Deno.env.get('SUPABASE_URL'),
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')
);
// Perform authorized operation...
});
Best Practice 5: Validate and Sanitize Inputs 5 min per form
RLS prevents unauthorized access, but you should still validate data:
-- Create a function with input validation
CREATE OR REPLACE FUNCTION create_post(
title TEXT,
content TEXT
) RETURNS posts AS $$
DECLARE
new_post posts;
BEGIN
-- Validate inputs
IF length(title) < 3 OR length(title) > 200 THEN
RAISE EXCEPTION 'Title must be between 3 and 200 characters';
END IF;
IF length(content) > 50000 THEN
RAISE EXCEPTION 'Content too long';
END IF;
-- Insert with auth.uid() for user_id
INSERT INTO posts (title, content, author_id)
VALUES (title, content, auth.uid())
RETURNING * INTO new_post;
RETURN new_post;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
Best Practice 6: Monitor and Audit Ongoing
Set up monitoring for your Supabase project:
- Database logs: Enable and review query logs in the dashboard
- Auth logs: Monitor failed login attempts and unusual patterns
- API usage: Watch for spikes that might indicate abuse
- RLS testing: Periodically test that policies work as expected
Testing RLS Policies
-- Test as a specific user
SET request.jwt.claim.sub = 'user-123-uuid';
-- Try to read data (should only see own data)
SELECT * FROM user_data;
-- Try to read another user's data (should fail or return empty)
SELECT * FROM user_data WHERE user_id = 'other-user-uuid';
-- Reset
RESET request.jwt.claim.sub;
Common Supabase Security Mistakes
| Mistake | Impact | Prevention |
|---|---|---|
| RLS disabled on tables | Full data exposure | Enable RLS before adding any data |
| Service key in client code | Complete database compromise | Only use in Edge Functions |
| Missing policy for operation | Users cannot perform needed actions | Test all CRUD operations |
| Overly permissive policies | Users access others' data | Always filter by auth.uid() |
| No email confirmation | Account enumeration, spam | Enable in Auth settings |
Official Resources: For the latest information, see Supabase RLS Documentation, Supabase Auth Guide, and Edge Functions Documentation.
Is the Supabase anon key safe to expose?
Yes, the anon key is designed to be public. It only allows operations that your RLS policies permit. The key itself provides no special access. Your security comes from RLS policies, not from keeping the anon key secret.
When should I use the service role key?
Only in server-side code like Edge Functions, API routes, or backend servers where you need to bypass RLS for administrative operations. Never in client-side code, browser applications, or anywhere the key could be exposed.
How do I test if my RLS policies work?
Use the Supabase SQL editor to set different JWT claims and test queries. Also test from your application as different users to verify they can only access their own data. The dashboard shows RLS status for each table.
Can I use Supabase for sensitive data?
Yes, Supabase is SOC 2 compliant and supports encryption. With proper RLS policies, authentication configuration, and following these best practices, Supabase is suitable for handling sensitive user data.
Verify Your Supabase Security
Scan your Supabase project for RLS issues and security misconfigurations.
Start Free Scan