Supabase Security Guide
Everything you need to secure your Supabase application. RLS policies, auth configuration, API key management, and common pitfalls to avoid.
Quick Security Check
Run this in your Supabase SQL editor to check if RLS is enabled on your tables:
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';
-- All tables should show rowsecurity = trueUnderstanding Supabase Security Model
Supabase security is built on three pillars:
Row Level Security
Database-level policies that control who can read/write each row
Authentication
User identity management with JWT tokens
API Keys
Anon key (public) vs Service key (private)
Critical Misconception
The Supabase anon key is not a secret. It's designed to be exposed in your frontend. Security comes from RLS policies, not from hiding this key.
Row Level Security (RLS) Deep Dive
Enable RLS on All Tables
RLS must be explicitly enabled on each table. Without it, anyone with the anon key can read all data.
-- Enable RLS on a table
ALTER TABLE "posts" ENABLE ROW LEVEL SECURITY;
-- Verify RLS is enabled
SELECT tablename, rowsecurity
FROM pg_tables
WHERE tablename = 'posts';Writing Effective RLS Policies
-- Re-evaluates for every row
CREATE POLICY "Users see own"
ON posts FOR SELECT
USING (auth.uid() = user_id);-- Evaluates once per query
CREATE POLICY "Users see own"
ON posts FOR SELECT
USING ((select auth.uid()) = user_id);Wrapping auth.uid() in a SELECT caches the result, significantly improving query performance.
Common RLS Patterns
Users own their data
CREATE POLICY "Users CRUD own data" ON "todos"
FOR ALL TO authenticated
USING ((select auth.uid()) = user_id)
WITH CHECK ((select auth.uid()) = user_id);Public read, owner write
CREATE POLICY "Anyone can read" ON "posts"
FOR SELECT USING (true);
CREATE POLICY "Owner can update" ON "posts"
FOR UPDATE TO authenticated
USING ((select auth.uid()) = author_id);Team-based access
CREATE POLICY "Team members access" ON "projects"
FOR ALL TO authenticated
USING (
EXISTS (
SELECT 1 FROM team_members
WHERE team_members.team_id = projects.team_id
AND team_members.user_id = (select auth.uid())
)
);API Key Security
| Key Type | Can be Public? | Respects RLS? | Use Case |
|---|---|---|---|
| anon key | Yes | Yes | Frontend client |
| service_role key | NO! | No (bypasses) | Server-side only |
Never Expose service_role Key
The service_role key bypasses all RLS policies. If exposed, attackers have full database access.
Where to Use Each Key
- • Frontend (React, Vue, etc.): Use anon key only
- • API Routes (Next.js, etc.): Can use service_role
- • Edge Functions: Can use service_role
- • Webhooks: Can use service_role
Common Supabase Security Mistakes
Disabling RLS for 'easier development'
Using service_role key in frontend
Forgetting RLS on new tables
SELECT policies without proper joins
Not restricting storage buckets
Frequently Asked Questions
Is it safe to expose my Supabase URL and anon key?
Yes, these are designed to be public. Your security comes from RLS policies, not from hiding these values. Think of them like a read-only API endpoint that respects user permissions.
How do I test if my RLS policies work?
Create two test users, log in as each, and try to access each other's data. Or use Supabase's SQL editor with 'SECURITY DEFINER' vs 'SECURITY INVOKER' to test policies.
Can I use Supabase without RLS?
Technically yes, but you'd need to put all database calls behind authenticated API routes and never use the client directly. RLS is almost always the better choice.
What about Supabase Edge Functions security?
Edge Functions run server-side and can safely use the service_role key. Validate inputs, use typed responses, and don't expose internal errors to clients.
Verify Your Supabase Security
Scan your Supabase application for misconfigurations, exposed keys, and RLS issues.
Get Starter ScanLast updated: January 2025