Security Guide

Supabase Security Checklist Before Launch

Check the security layers your app actually depends on: RLS and grants, Auth, publishable and secret keys, Storage policies, RPC functions, and Edge Functions.

Use this as a pre-launch review, then follow the dedicated RLS guide when you need copyable policies and cross-user tests.

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 = true

Prefer not to run SQL by hand? The free Supabase RLS checker tests your live app's tables for anonymous read and write access in about a minute.

Understanding 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

Bad: Performance issues
-- Re-evaluates for every row
CREATE POLICY "Users see own"
ON posts FOR SELECT
USING (auth.uid() = user_id);
Good: Cached evaluation
-- 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 TypeCan be Public?Respects RLS?Use Case
Publishable key (or legacy anon key)YesYesFrontend client
Secret key (or legacy service_role key)NO!No (bypasses)Server-side only

Never Expose Secret or service_role Keys

Supabase secret keys and legacy service_role keys have elevated server-side access and must never ship to a browser or mobile client.

Where to Use Each Key

  • • Frontend (React, Vue, etc.): Use a publishable key or legacy anon key
  • • API Routes: Use a secret or legacy service_role key only when elevated access is required
  • • Edge Functions: Keep privileged keys in server-side secrets
  • • Webhooks: Verify signatures before using privileged database access

Common Supabase Security Mistakes

Disabling RLS for 'easier development'

Impact: Anyone can read/write all data in your database
Fix: Always develop with RLS enabled. Use service_role in seed scripts if needed.

Using service_role key in frontend

Impact: Full database access for anyone who inspects your code
Fix: Only use anon key in frontend. Move privileged operations to API routes.

Forgetting RLS on new tables

Impact: New features ship with exposed data
Fix: Create a migration template that always includes RLS.

SELECT policies without proper joins

Impact: N+1 query performance issues, policy bypasses
Fix: Use EXISTS subqueries for related table checks.

Not restricting storage buckets

Impact: Users can access/overwrite other users' files
Fix: Configure storage policies like table RLS.

Frequently Asked Questions

Is it safe to expose my Supabase URL and anon key?

Yes. The project URL and publishable key, or legacy anon key, are designed for public clients. They identify the project but do not make access safe on their own. Your RLS policies, grants, Storage policies, and server-side authorization still have to enforce the intended 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 use a secret or legacy service_role key when elevated access is genuinely required. First validate the caller and input, scope each operation narrowly, keep keys in server-side secrets, and do not expose internal errors to clients.

Verify Your Supabase Security

Scan your Supabase application for misconfigurations, exposed keys, and RLS issues.

Run your first scan free

Last updated: September 4, 2026