How to Secure Your Supabase App in 10 Minutes
Supabase is the go-to backend for vibe coders. Lovable, Bolt, and dozens of AI coding tools default to Supabase for auth, database, and storage. It's fast to set up, generous on the free tier, and pairs perfectly with frameworks like Next.js and React.
But there's a problem: most Supabase apps ship with critical security misconfigurations. We've scanned hundreds of production apps with ScanVibe, and the same issues come up again and again.
The good news? You can fix all of them in about 10 minutes. Here's how.
1. Enable Row Level Security (RLS) on Every Table
This is the #1 Supabase security issue we see. Without RLS, anyone with your anon key can read and write every row in your database.
The Problem
When you create a table in Supabase, RLS is disabled by default. That means your anon API key — which is embedded in your frontend JavaScript — gives full access to all data.
-- Check which tables have RLS disabled
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';
The Fix
Enable RLS on every table, then add policies for what should be accessible:
-- Enable RLS
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
-- Allow users to read their own profile
CREATE POLICY "Users can read own profile"
ON public.profiles FOR SELECT
USING (auth.uid() = id);
-- Allow users to update their own profile
CREATE POLICY "Users can update own profile"
ON public.profiles FOR UPDATE
USING (auth.uid() = id);
Common Patterns
For tables that should be publicly readable but only editable by owners:
-- Anyone can read
CREATE POLICY "Public read" ON public.posts
FOR SELECT USING (true);
-- Only the author can insert/update/delete
CREATE POLICY "Author can modify" ON public.posts
FOR ALL USING (auth.uid() = author_id);
For private tables (e.g., user settings, payment info):
-- Only authenticated users can access their own rows
CREATE POLICY "Private access" ON public.user_settings
FOR ALL USING (auth.uid() = user_id);
2. Stop Exposing Your Service Role Key
Your Supabase project has two keys:
The Problem
We frequently find the service_role key in:
- Frontend JavaScript bundles
.envfiles committed to public repos- Client-side Supabase client initialization
The Fix
- Check your environment variables. The service role key should only be in server-side env vars (no
NEXT_PUBLIC_prefix in Next.js).
# WRONG — exposed to the browser
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY=eyJhb...
# RIGHT — server-side only
SUPABASE_SERVICE_ROLE_KEY=eyJhb...
- Use the anon key in your frontend client:
// Frontend client — uses anon key
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
- Use the service role key only in server-side code (API routes, server actions):
// Server-side only — never import this in client code
import { createClient } from '@supabase/supabase-js';
const supabaseAdmin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
3. Secure Your Storage Buckets
The Problem
Many apps create public buckets or have storage policies that allow anyone to upload or overwrite files. We've seen:
- Buckets where any authenticated user can read all files
- Upload policies with no file size or type restrictions
- No policies at all (which means no access — or full access if the bucket is public)
The Fix
-- Only allow users to access their own files
CREATE POLICY "Users access own files" ON storage.objects
FOR ALL USING (
bucket_id = 'avatars'
AND auth.uid()::text = (storage.foldername(name))[1]
);
• Use user-specific folders:
avatars/{user_id}/photo.jpg• Set file size limits in your upload logic
• Validate file types server-side
• Use signed URLs for private files instead of making buckets public
4. Configure Auth Properly
Supabase Auth is solid out of the box, but there are common misconfigurations.
Disable Email Confirmations Carefully
If you disable email confirmations (common during development), remember to re-enable them before going to production. Without confirmation, anyone can create accounts with fake emails.
Restrict Redirect URLs
In your Supabase dashboard under Authentication > URL Configuration:
# Add only your production domain and localhost
https://yourdomain.com/**
http://localhost:3000/**
https://* — this allows redirect attacks where an attacker can steal auth tokens by redirecting to their own domain.Enable Rate Limiting
Supabase has built-in rate limiting for auth endpoints. Make sure it's enabled:
- Go to Authentication > Rate Limits
- Set reasonable limits (e.g., 10 sign-ups per hour per IP)
5. Audit Your Database Functions
If you use Supabase Edge Functions or database functions, they can bypass RLS if they use SECURITY DEFINER.
The Problem
-- This function runs with the creator's permissions, not the caller's
CREATE FUNCTION get_all_users()
RETURNS SETOF public.profiles
LANGUAGE sql
SECURITY DEFINER -- Dangerous: bypasses RLS
AS $$
SELECT * FROM public.profiles;
$$;
The Fix
Use SECURITY INVOKER unless you have a specific reason not to:
CREATE FUNCTION get_user_profile(user_id uuid)
RETURNS public.profiles
LANGUAGE sql
SECURITY INVOKER -- Safe: respects RLS
AS $$
SELECT * FROM public.profiles WHERE id = user_id;
$$;
If you must use SECURITY DEFINER, add explicit permission checks inside the function.
Quick Checklist
Run through this in 10 minutes:
- RLS enabled on all tables in public schema
- RLS policies defined for each table (no "allow all" unless intentional)
service_rolekey is NOT in anyNEXT_PUBLIC_env var or frontend code.envfiles are in.gitignore- Storage buckets have appropriate policies
- Auth redirect URLs are restricted to your domains
- Email confirmation is enabled in production
- Database functions use
SECURITY INVOKERby default - Rate limiting is configured for auth endpoints
Automate It with ScanVibe
Don't want to check all this manually every time you deploy? ScanVibe scans your Supabase app for all these issues automatically. We check:
One scan. 30 seconds. All the issues found.