Supabase Security Checklist: Protect Your Database in Production
Supabase powers thousands of apps, but misconfigured RLS, exposed service keys, and open storage buckets put your data at risk. Here's how to lock it down.
By Gabriel CA · Kraftwire Software
· 9 min readWhy Supabase Security Needs Your Attention
Supabase provides a Postgres database, authentication, file storage, and Edge Functions out of the box. It is a powerful platform with strong security fundamentals. But those fundamentals only protect you if you configure them correctly.
The biggest misconception about Supabase is that it is secure by default. It is not. Supabase gives you the tools to build secure applications, but the configuration is your responsibility. This guide walks through every security setting you need to configure before your app goes to production.
Row-Level Security (RLS)
RLS is the foundation of Supabase security. It controls who can read and write data at the database level. Without RLS, your tables are accessible to anyone with your project URL and anon key, both of which are public.
The Most Common Supabase Mistake
Creating a table without enabling RLS is the number one security mistake in Supabase projects. When RLS is disabled, the anon key (which is embedded in your frontend code and visible to everyone) provides unrestricted access to the entire table.
**What this means in practice:** An attacker opens your app, finds the Supabase URL and anon key in the JavaScript bundle (they are always there, by design), and uses them to query your tables directly. Without RLS, they can read every record, modify any data, and delete whatever they want.
How to Enable RLS
-- Enable RLS on your table
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
-- Allow users to read only their own profile
CREATE POLICY "Users can read own profile"
ON public.profiles FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
-- Allow users to update only their own profile
CREATE POLICY "Users can update own profile"
ON public.profiles FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
RLS Best Practices
**Always use `auth.uid()` to scope data to the current user.** Never trust user IDs sent from the client. The `auth.uid()` function reads the user ID from the verified JWT token, which cannot be forged.
**Create separate policies for each operation.** Having different policies for SELECT, INSERT, UPDATE, and DELETE gives you fine-grained control. A user who can read data should not necessarily be able to delete it.
**Test your policies.** After creating RLS policies, test them by:
Querying as an unauthenticated user (should return nothing for private data)
Querying as User A and trying to access User B's data (should be blocked)
Trying to modify data through direct API calls, not just through your UI
**Watch out for the `public` role.** Policies applied to the `public` role affect unauthenticated users. Only use this for genuinely public data like published blog posts or product listings.
Service Role Key Protection
Supabase provides two keys: the **anon key** and the **service role key**. Understanding the difference is critical.
Anon Key (Safe for Frontend)
The anon key is designed to be public. It is included in your frontend code, and that is fine. Its access is limited by your RLS policies. Think of it as a key that opens the front door but only lets you into rooms you are authorized to enter.
Service Role Key (Never Expose)
The service role key bypasses all RLS policies. It has full, unrestricted access to every table, every row, and every operation. If an attacker gets this key, they own your entire database.
**Where the service role key should live:**
Server-side Edge Functions (via environment variables)
Backend server code (via environment variables)
CI/CD pipelines (via secrets management)
**Where the service role key should NEVER be:**
Frontend JavaScript code
Environment variables prefixed with `VITE_`, `NEXT_PUBLIC_`, or `REACT_APP_`
Git repositories (even private ones)
Client-side configuration files
Browser-accessible API calls
How to Check for Exposure
Search your codebase for the service role key value. If you find it anywhere in your `src` directory or in any file that gets bundled for the browser, it is exposed and needs to be moved immediately.
Authentication Configuration
Email Confirmation
By default, Supabase can be configured to auto-confirm email signups. This is convenient for development but dangerous in production because it allows anyone to create accounts with fake email addresses.
**Enable email confirmation for production:** Go to Authentication settings and disable auto-confirm. Users should verify their email before gaining access to your application.
Password Requirements
Set minimum password requirements to prevent users from choosing weak passwords:
Minimum 8 characters (12 is better)
Consider requiring a mix of character types
Use Supabase's leaked password protection to block passwords known to be compromised
Rate Limiting on Auth Endpoints
Supabase has built-in rate limiting for authentication endpoints, but verify that it is configured appropriately. Brute force protection should limit login attempts per IP address.
Storage Security
Supabase Storage handles file uploads and downloads. Like database tables, storage buckets need security configuration.
Public vs Private Buckets
**Public buckets** serve files without authentication. Use for assets like images and documents that should be accessible to everyone.
**Private buckets** require authentication and storage policies to access files. Use for user uploads, private documents, and sensitive files.
Storage Policies
-- Allow users to upload to their own folder
CREATE POLICY "Users can upload to own folder"
ON storage.objects FOR INSERT
TO authenticated
WITH CHECK (
bucket_id = 'user-uploads' AND
(storage.foldername(name))[1] = auth.uid()::text
);
-- Allow users to read their own files
CREATE POLICY "Users can read own files"
ON storage.objects FOR SELECT
TO authenticated
USING (
bucket_id = 'user-uploads' AND
(storage.foldername(name))[1] = auth.uid()::text
);
File Upload Validation
Restrict file types to what your application actually needs
Set maximum file size limits
Validate file content, not just the extension (an attacker can rename a script to `.jpg`)
Scan uploaded files for malware if your application handles sensitive content
Edge Functions Security
Edge Functions run server-side code on Supabase's infrastructure. They are the right place for operations that need the service role key, third-party API calls, or custom business logic.
JWT Verification
By default, Edge Functions verify the JWT token from the request. This means only authenticated users can call them. If you disable JWT verification (setting `verify_jwt = false`), the function is accessible to anyone.
**Only disable JWT verification when:**
The function is a webhook receiver that gets called by external services
The function handles its own authentication through a different mechanism
The function is genuinely public (like a health check endpoint)
Secret Management
Store API keys and credentials as Supabase secrets, not in your Edge Function code:
// Correct - reads from secure environment
const apiKey = Deno.env.get("OPENAI_API_KEY");
// Wrong - hardcoded in source code
const apiKey = "sk-abc123...";
CORS Configuration
Edge Functions should return appropriate CORS headers. Restrict the `Access-Control-Allow-Origin` to your frontend domain rather than using a wildcard:
const corsHeaders = {
"Access-Control-Allow-Origin": "https://yourdomain.com",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "authorization, content-type",
};
Database Function Security
When creating database functions that need elevated privileges (like checking user roles), use `SECURITY DEFINER` carefully:
CREATE OR REPLACE FUNCTION public.has_role(_user_id uuid, _role app_role)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $
SELECT EXISTS (
SELECT 1 FROM public.user_roles
WHERE user_id = _user_id AND role = _role
)
$;
**Always set `search_path`** on security definer functions to prevent search path injection attacks. Without it, an attacker might be able to create a function with the same name in a different schema and trick your function into calling it.
The Complete Supabase Security Checklist
Database
RLS enabled on every table
Policies created for SELECT, INSERT, UPDATE, and DELETE operations
Policies use `auth.uid()` to scope data to the current user
Admin operations use the `has_role()` function pattern
No tables accessible without authentication (unless intentionally public)
Database functions use `SECURITY DEFINER` with `search_path` set
Authentication
Email confirmation enabled (auto-confirm disabled)
Password requirements configured (minimum length, complexity)
Leaked password protection enabled
Rate limiting active on auth endpoints
MFA available for sensitive applications
Keys and Secrets
Service role key never in frontend code
Service role key not in any `VITE_` or `NEXT_PUBLIC_` variables
API keys stored as Supabase secrets, not hardcoded
Anon key properly scoped through RLS policies
Storage
Private buckets used for user-specific files
Storage policies enforce user-scoped access
File type and size restrictions configured
Public buckets only used for genuinely public assets
Edge Functions
JWT verification enabled unless explicitly needed otherwise
CORS restricted to your frontend domain
Secrets accessed through environment variables
Input validation on all function parameters
Error responses do not leak implementation details
Monitoring
Failed auth attempts monitored
Unusual database query patterns flagged
Storage usage tracked for abuse
Edge Function errors logged and reviewed
Scan Your Supabase App
SimplyScan includes Supabase-specific security checks in every scan. It verifies RLS configuration, checks for exposed service role keys, validates auth settings, and tests for common Supabase misconfiguration patterns.
[Scan your app now](/)
Related Guides
[RLS Policies Explained](/blog/rls-policies-explained)
[Architecture Security Risks](/blog/architecture-security-risks)
[Environment Variables Security](/blog/environment-variables-security)
[Is Lovable Safe?](/blog/is-lovable-safe)