Supabase Security Best Practices: RLS, Auth, and API Protection

Share

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 (do this for 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

Users can only access their own data
-- 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

Blog posts example
-- 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 access shared data
-- 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:

KeyPurposeWhere to UseSecurity
anon (public)Client-side operationsBrowser, mobile appsSafe to expose, RLS enforced
service_roleAdmin operationsServer-side onlyNever 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

Client-side auth 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:

Secure Edge Function pattern
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:

Input validation with database functions
-- 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 your 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

MistakeImpactPrevention
RLS disabled on tablesFull data exposureEnable RLS before adding any data
Service key in client codeComplete database compromiseOnly use in Edge Functions
Missing policy for operationUsers cannot perform needed actionsTest all CRUD operations
Overly permissive policiesUsers access others' dataAlways filter by auth.uid()
No email confirmationAccount enumeration, spamEnable 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
Best Practices

Supabase Security Best Practices: RLS, Auth, and API Protection