Supabase
Security Guide

How to Secure Your Supabase App

Last updated: January 12, 2026

Supabase is secure by design, but requires proper configuration. This guide covers the essential security steps for any Supabase-powered application.

Why Security Matters for Supabase

Key Security Concerns

83% of Supabase exposures involve RLS misconfiguration (Escape.tech research)
RLS is NOT enabled by default - tables are publicly accessible until you enable it
Developers confuse anon key (safe to expose) with service_role key (NEVER expose)
Using auth.uid() instead of (select auth.uid()) causes performance issues at scale
CVE-2025-48757 showed 170+ Lovable apps with RLS misconfiguration

Security Strengths

PostgreSQL RLS is battle-tested enterprise technology (unlike Firebase's proprietary rules)
SOC 2 Type II compliant with regular third-party security audits
Two-key architecture: anon key (public) vs service_role key (secret) makes security explicit
SQL-based policies are version-controllable and reviewable in code
Edge Functions keep server-side logic truly server-side

Step-by-Step Security Guide

1. Enable RLS on All Tables

Row Level Security is the foundation of Supabase security. Enable it on every table that contains data.

ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

2. Write Restrictive Policies

Use the (select auth.uid()) pattern for better performance. Test policies with different user contexts.

CREATE POLICY "Users read own profile" ON profiles
  FOR SELECT TO authenticated
  USING ((select auth.uid()) = id);

3. Protect Service Role Key

The service_role key bypasses RLS. Never expose it in frontend code. Use it only in server-side functions.

4. Secure RPC Functions

Database functions should check authentication. Use SECURITY DEFINER carefully.

CREATE FUNCTION get_user_data()
RETURNS json AS $$
BEGIN
  IF auth.uid() IS NULL THEN
    RAISE EXCEPTION 'Not authenticated';
  END IF;
  -- function logic
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

5. Configure Auth Settings

Enable email confirmation, set password requirements, and configure rate limiting in the Supabase dashboard.

6. Test Your Policies

Query your database as an anonymous user to verify RLS is working. VAS does this automatically.

Common Security Mistakes

Avoid these common Supabase security pitfalls:

Creating tables without enabling RLS
Using auth.uid() instead of (select auth.uid())
Exposing service_role key in client code
Creating overly permissive policies
Not testing RLS with real queries

Known Supabase Vulnerabilities

These are documented security issues specific to Supabase applications. Click through for detailed remediation guidance.

Recommended Security Tools

Use these tools to maintain security throughout development:

VAS Security Scanner
npm audit / yarn audit
Git-secrets
Snyk

Ready to Secure Your App?

Security is an ongoing process, not a one-time checklist. After implementing these steps, use VAS to verify your Supabase app is secure before launch, and consider regular scans as you add new features.

Frequently Asked Questions

What's the difference between auth.uid() and (select auth.uid())?

Both return the current user's ID, but (select auth.uid()) is a subquery that evaluates once per query, while auth.uid() re-evaluates for every row. Use (select auth.uid()) in RLS policies for better performance, especially on large tables.

Do I need RLS if I only query from server-side code?

If you use service_role key (server-side), RLS is bypassed anyway. But if ANY part of your app uses the anon key or calls Supabase from the browser, you MUST have RLS. Best practice: always enable RLS even for server-only access, as defense in depth.

How do I debug RLS policies not working?

1) Check if RLS is actually enabled: SELECT relrowsecurity FROM pg_class WHERE relname = 'your_table'. 2) Check policies exist: SELECT * FROM pg_policies WHERE tablename = 'your_table'. 3) Test with SQL Editor using different roles (anon, authenticated).

Can I use RLS for multi-tenant apps?

Yes, RLS is perfect for multi-tenancy. Create a tenant_id column, then write policies like: USING ((select auth.jwt() ->> 'tenant_id') = tenant_id). This ensures users can only access data from their organization, enforced at the database level.