CyberCheckerSECURITY SCANNER
Home/Supabase Security Checklist
Database Security

Supabase Security Checklist: Row Level Security Guide for Startups in 2026

15 min read
Essential

CRITICAL: Your Supabase Database Might Be Wide Open

Without Row Level Security (RLS) enabled, your Supabase anon key gives anyone full read/write access to your entire database. Competitors have scraped user lists. Attackers have deleted tables. One startup lost 40% of their users after a breach.

Supabase is incredible for rapid development. But its security model is fundamentally different from traditional databases. The anon key is meant to be public—it's in your frontend code, visible to anyone.

This works perfectly... if you enable Row Level Security. Without RLS, that public anon key becomes a skeleton key to your entire database.

This guide shows you exactly how to secure your Supabase database, check if you're vulnerable, and implement proper RLS policies in 2026.

Understanding Supabase Security Model

The Anon Key Paradox

Supabase gives you two API keys:

Anon Key (Public)

✓ Safe to expose in frontend code

✓ Used for client-side operations

⚠️ Respects RLS policies

Service Role Key (Secret)

✗ NEVER expose in frontend

✓ Server-side only

⚠️ Bypasses ALL RLS policies

The key insight: The anon key is designed to be public. Supabase's entire security model depends on Row Level Security policies controlling what that public key can access.

How RLS Works

Row Level Security is a PostgreSQL feature that Supabase uses for authorization:

1

User makes request with anon key

From your React/Vue/Next.js frontend

2

Supabase checks RLS policies

SQL policies you define in your database

3

Policy evaluates user permissions

Based on auth.uid(), user role, or custom logic

4

Allow or deny the operation

User only sees/modifies data they're authorized for

-- Example: Users can only read their own data
CREATE POLICY "Users can view own profile"
  ON profiles
  FOR SELECT
  USING (auth.uid() = user_id);

-- Without this policy + RLS enabled:
-- Anyone with anon key can SELECT * FROM profiles 🚨

Why RLS is Critical (Real Attack Scenarios)

Scenario 1: Competitor Scrapes Your User Base

The Attack: Competitor opens your app's DevTools, finds your Supabase URL and anon key (it's in every network request). They write a simple script:

const { data } = await supabase
  .from('users')
  .select('email, name, company, plan')
  .limit(10000);

// If RLS is disabled: Returns ALL your users 🚨
// If RLS is enabled: Returns empty array ✅

Real case: SaaS startup lost 40% of users when competitor offered them discounts using scraped data.

Scenario 2: Malicious User Deletes Everything

The Attack: Disgruntled user (or attacker) runs:

await supabase
  .from('posts')
  .delete()
  .neq('id', 0); // Delete everything

// Without RLS: All posts deleted 🚨
// With RLS: Only their own posts deleted ✅

Real case: E-commerce site had all product listings deleted. No backups. Business destroyed.

Scenario 3: Privacy Violation

The Attack: User accesses other users' private messages:

const { data } = await supabase
  .from('messages')
  .select('*')
  .eq('recipient_id', 'some-other-user-id');

// Without RLS: Reads anyone's messages 🚨
// With RLS: Only reads messages where they're sender/recipient ✅

Real case: Dating app exposed all private conversations. GDPR fine + lawsuit + reputation destroyed.

How to Check If You're Vulnerable

Method 1: Supabase Dashboard (Quick Check)

  1. 1.

    Open Supabase Dashboard

    Go to Authentication → Policies

  2. 2.

    Check each table

    Look for "RLS enabled" badge

  3. 3.

    Verify policies exist

    Each table should have at least one policy

Red flags: If you see "RLS disabled" or "No policies" on any table with user data, you're vulnerable.

Method 2: SQL Query (Comprehensive)

Run this in your SQL Editor to check all tables:

SELECT 
  schemaname,
  tablename,
  rowsecurity as rls_enabled
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;

-- Look for rowsecurity = false (RLS disabled) 🚨

Check which tables have policies:

SELECT 
  tablename,
  policyname,
  cmd,
  qual
FROM pg_policies
WHERE schemaname = 'public'
ORDER BY tablename;

-- Empty result = No policies = Vulnerable 🚨

Method 3: Test Attack (Safe)

Open your browser console on your app and try to access data you shouldn't:

// Log out or open incognito
// Then try to access protected data
const { data, error } = await supabase
  .from('users')
  .select('*');

console.log(data);

// If you see data: RLS is broken 🚨
// If you see empty array or error: RLS works ✅

Warning: Only do this on your own app. Testing on others' apps without permission is illegal.

Method 4: Automated Scan

Use CyberChecker to automatically detect RLS issues:

  • Checks if Supabase anon key is exposed
  • Tests if RLS is properly configured
  • Shows exact security score
  • Provides fix recommendations
Scan Your Site for RLS Issues - Free

Implementing RLS Policies Step-by-Step

Step 1: Enable RLS on All Tables

First, enable RLS for every table with user data:

-- Enable RLS on your tables
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- Do this for EVERY table 🔒

Critical: Just enabling RLS blocks ALL access by default. You MUST add policies next, or your app will break.

Step 2: Create Basic Policies

Start with the most common pattern: users can only access their own data.

-- Allow users to read their own profile
CREATE POLICY "Users can view own profile"
  ON profiles
  FOR SELECT
  USING (auth.uid() = user_id);

-- Allow users to update their own profile
CREATE POLICY "Users can update own profile"
  ON profiles
  FOR UPDATE
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

-- Allow users to insert their own profile
CREATE POLICY "Users can insert own profile"
  ON profiles
  FOR INSERT
  WITH CHECK (auth.uid() = user_id);

Step 3: Understanding Policy Types

FOR SELECT (Read)

Controls who can query data

USING (auth.uid() = user_id)

FOR INSERT (Create)

Controls who can create new rows

WITH CHECK (auth.uid() = user_id)

FOR UPDATE (Modify)

Controls who can update existing rows

USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id)

FOR DELETE (Remove)

Controls who can delete rows

USING (auth.uid() = user_id)

Common RLS Patterns for Startups

Pattern 1: Public Read, Authenticated Write

Use for: Blog posts, product listings, public profiles

-- Anyone can read
CREATE POLICY "Public read access"
  ON posts
  FOR SELECT
  USING (true);

-- Only authenticated users can create
CREATE POLICY "Authenticated users can create"
  ON posts
  FOR INSERT
  TO authenticated
  WITH CHECK (auth.uid() = author_id);

-- Only author can update/delete
CREATE POLICY "Authors can update own posts"
  ON posts
  FOR UPDATE
  USING (auth.uid() = author_id);

Pattern 2: Team/Organization Access

Use for: SaaS apps with teams/workspaces

-- Users can access data from their organization
CREATE POLICY "Team members can view team data"
  ON projects
  FOR SELECT
  USING (
    organization_id IN (
      SELECT organization_id 
      FROM team_members 
      WHERE user_id = auth.uid()
    )
  );

-- Only team admins can delete
CREATE POLICY "Admins can delete"
  ON projects
  FOR DELETE
  USING (
    EXISTS (
      SELECT 1 FROM team_members
      WHERE user_id = auth.uid()
      AND organization_id = projects.organization_id
      AND role = 'admin'
    )
  );

Pattern 3: Private Messages/DMs

Use for: Chat apps, messaging systems

-- Users can only read messages they sent or received
CREATE POLICY "Users can view own messages"
  ON messages
  FOR SELECT
  USING (
    auth.uid() = sender_id 
    OR auth.uid() = recipient_id
  );

-- Users can only send messages as themselves
CREATE POLICY "Users can send messages"
  ON messages
  FOR INSERT
  WITH CHECK (auth.uid() = sender_id);

-- Users can only delete their own sent messages
CREATE POLICY "Users can delete own sent messages"
  ON messages
  FOR DELETE
  USING (auth.uid() = sender_id);

Pattern 4: Subscription/Plan-Based Access

Use for: Feature gating, premium content

-- Free users see limited features
CREATE POLICY "Free users limited access"
  ON features
  FOR SELECT
  USING (
    is_free = true 
    OR (
      SELECT plan FROM users 
      WHERE id = auth.uid()
    ) IN ('pro', 'enterprise')
  );

-- Pro users see everything
CREATE POLICY "Pro users full access"
  ON features
  FOR SELECT
  USING (
    (SELECT plan FROM users WHERE id = auth.uid()) 
    IN ('pro', 'enterprise')
  );

Advanced RLS Techniques in 2026

For complex authorization scenarios, you'll need advanced RLS patterns. This deep dive covers everything from basic policies to production-grade security:

Key Topics Covered:

  • Multi-table policy joins
  • Role-based access control (RBAC)
  • Performance optimization
  • Testing complex policies
  • Security functions
  • Edge cases and gotchas

Testing Your RLS Policies

⚠️ Never deploy RLS policies without testing!

Broken policies can either lock out legitimate users or expose data to attackers. Always test before production.

Testing Method 1: Browser Console

  1. 1.

    Create test users

    Sign up with multiple test accounts (user A, user B)

  2. 2.

    Log in as user A

    Create some test data

  3. 3.

    Log in as user B

    Try to access user A's data in browser console:

    const { data } = await supabase
      .from('profiles')
      .select('*')
      .eq('user_id', 'user-a-id');
    
    // Should return empty [] ✅
  4. 4.

    Try to modify user A's data

    const { error } = await supabase
      .from('profiles')
      .update({ name: 'hacked' })
      .eq('user_id', 'user-a-id');
    
    // Should get permission denied error ✅

Testing Method 2: SQL Functions

Test policies as different users using SQL:

-- Switch to a specific user context
SET request.jwt.claim.sub = 'user-a-uuid';

-- Test if they can see their own data
SELECT * FROM profiles WHERE user_id = 'user-a-uuid';
-- Should return data ✅

-- Test if they can see other user's data
SELECT * FROM profiles WHERE user_id = 'user-b-uuid';
-- Should return nothing ✅

-- Reset to anon
RESET request.jwt.claim.sub;

Testing Checklist

Users can read their own data
Users CANNOT read other users' data
Users can create new records (if allowed)
Users can update their own data
Users CANNOT update other users' data
Users can delete their own data (if allowed)
Users CANNOT delete other users' data
Unauthenticated users have appropriate access
Service role key bypasses RLS (expected)
Policies work correctly for team/organization scenarios

Complete Supabase Security Checklist 2026

Essential Security Measures

RLS enabled on ALL tablesCRITICAL
At least one policy per tableCRITICAL
Tested policies with multiple user accountsCRITICAL
Service role key NEVER in frontend codeCRITICAL
Anon key properly exposed (it's meant to be public)
Auth configured (email/password, OAuth, magic links)
Database backups enabled
SSL enforced on database connections
Password policies configured
Rate limiting on auth endpoints
Email templates customized
CORS properly configured
Realtime security rules defined
Storage bucket policies configured
Edge Functions have proper auth checks

Recommended Tools & Resources

Supabase Dashboard

Authentication → Policies to view and manage RLS

pgAdmin or TablePlus

SQL clients for advanced policy testing

Supabase CLI

Version control your database schema and policies

CyberChecker

Automated Supabase security scanning

Check Your Supabase Security

Frequently Asked Questions

Is it safe to expose my Supabase anon key?

Yes, the anon key is designed to be public and safe to use in frontend code. However, you MUST have Row Level Security (RLS) policies enabled on all tables. Without RLS, the anon key gives unrestricted database access.

What happens if I enable RLS but forget to add policies?

Your app will break. Enabling RLS without policies blocks ALL access to that table, including legitimate users. You must create at least one policy per table after enabling RLS.

Can I use the service role key in my frontend?

Absolutely not. The service role key bypasses ALL RLS policies and should only be used in server-side code. Exposing it in frontend code gives attackers full database access.

How do I fix 'new row violates row-level security policy' errors?

This error means your INSERT policy's WITH CHECK clause is rejecting the data. Common cause: trying to insert a user_id that doesn't match auth.uid(). Verify your WITH CHECK logic matches the data being inserted.

Do RLS policies affect performance?

Yes, complex policies can slow queries. Optimize by: using indexes on columns in USING/WITH CHECK clauses, keeping policies simple, avoiding subqueries when possible, and testing with EXPLAIN ANALYZE.

How often should I audit my RLS policies?

Review policies whenever you: add new tables, change user roles/permissions, add new features, or at least quarterly. Use automated tools like CyberChecker for continuous monitoring.

Conclusion

Supabase's security model is powerful but requires understanding. The anon key being public is a feature, not a bug—but only when Row Level Security is properly configured.

The three most common mistakes in 2026:

  1. Forgetting to enable RLS on new tables
  2. Enabling RLS but not creating policies (app breaks)
  3. Creating policies but not testing them (data leaks)

Avoid all three by making RLS part of your development workflow: enable RLS when creating tables, write policies before deploying, and test with multiple user accounts before going live.

Next Steps:

  1. 1.Audit your current setup - Check which tables have RLS disabled
  2. 2.Enable RLS on all tables - Start with read-only tables first
  3. 3.Write basic policies - Use the patterns above as templates
  4. 4.Test thoroughly - Create test users and try to break your policies
  5. 5.Monitor continuously - Use automated scanning tools

Is Your Supabase Database Properly Secured?

CyberChecker automatically detects RLS issues, exposed credentials, and other Supabase security problems. Get your security report in 60 seconds.

Scan Your Supabase App - Free

Published by CyberChecker Security Team

Last updated: