Every table in the public schema must have Row Level Security enabled with at least one policy per operation. Without RLS, the Supabase API exposes every row to every request — any browser with your anon key or any logged-in user can read, modify, or delete data belonging to other users. A single table missing RLS can leak your entire user base's private data or let one user overwrite another's records.
Why This Matters
Without RLS, any user with the anon key or an authenticated session can read, insert, update, or delete every row in the table via the Supabase client or a direct PostgREST request. A single unprotected table can expose your entire user base's data. This is the #1 most common Supabase security vulnerability.
Supabase exposes every table in the public schema through PostgREST (the auto-generated REST API) and Realtime. If a table does not have Row Level Security enabled, any user with the anon key or an authenticated session can perform any operation on any row — regardless of whether your application code restricts access.
This is not a theoretical risk. It is the single most common vulnerability in Supabase projects.
The rule
Every table in the public schema must have:
ALTER TABLE <table> ENABLE ROW LEVEL SECURITY;
At least one policy for each operation the table supports (SELECT, INSERT, UPDATE, DELETE)
A restrictive default — if no policy matches, the operation is denied
Bad example
CREATE TABLE public.documents ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), owner_id uuid REFERENCES auth.users(id), title text NOT NULL, content text);-- No RLS! Any user with the anon key can read/write all documents.
Good example
CREATE TABLE public.documents ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), owner_id uuid REFERENCES auth.users(id), title text NOT NULL, content text);ALTER TABLE public.documents ENABLE ROW LEVEL SECURITY;-- Users can only see their own documentsCREATE POLICY "users read own documents" ON public.documents FOR SELECT USING (owner_id = auth.uid());-- Users can only insert documents they ownCREATE POLICY "users insert own documents" ON public.documents FOR INSERT WITH CHECK (owner_id = auth.uid());-- Users can only update their own documentsCREATE POLICY "users update own documents" ON public.documents FOR UPDATE USING (owner_id = auth.uid()) WITH CHECK (owner_id = auth.uid());-- Users can only delete their own documentsCREATE POLICY "users delete own documents" ON public.documents FOR DELETE USING (owner_id = auth.uid());
Common mistakes
Forgetting new tables — every migration that adds a table must also enable RLS and add policies
Overly permissive policies — a policy like USING (true) grants access to everyone, defeating the purpose of RLS. Always scope policies to the authenticated user with auth.uid().
Missing operation coverage — having a SELECT policy but no INSERT/UPDATE/DELETE policies means read access is controlled but writes are wide open (denied by default only if RLS is enabled)
RLS enabled but no policies — this is the opposite problem: RLS with zero policies blocks all access, including your own application. Always add at least one policy per operation.
How to detect
The most reliable detection is querying the database directly:
Any rows returned are unprotected tables. Run this in CI after migrations:
# Fails if any public table lacks RLSresult=$(psql "$DATABASE_URL" -t -c " SELECT count(*) FROM pg_tables WHERE schemaname = 'public' AND rowsecurity = false;")if [ "$result" -gt 0 ]; then echo "FAIL: $result tables without RLS" exit 1fi
For code review, search migration files for CREATE TABLE and verify a matching ENABLE ROW LEVEL SECURITY exists.
Remediation
Enable RLS: ALTER TABLE public.<table> ENABLE ROW LEVEL SECURITY;
Add policies for every operation the table needs
Test with a non-service-role client to confirm access is restricted
Add the pg_tables CI check above to prevent regressions