Fixing Supabase RLS
Enable and configure Row Level Security to protect your database tables.
The problem
When AI tools scaffold a Supabase app, they often create tables without enabling Row Level Security (RLS). This means anyone with your Supabase anon key can read every row in the table — no authentication required.
Your anon key is always visible in client-side JavaScript. It's designed to be public. RLS is the mechanism that makes this safe.
Without RLS, your Supabase anon key is effectively an admin key. Anyone who opens DevTools can query your entire database.
How to fix it
Enable RLS on every table
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.messages ENABLE ROW LEVEL SECURITY;
-- repeat for every tableEnabling RLS without adding policies will block all access (even authenticated). This is safe — you then add policies to grant specific access.
Add read policies
Allow users to read their own data:
CREATE POLICY "Users can read own profile"
ON public.profiles
FOR SELECT
USING (auth.uid() = user_id);For tables with org/team membership:
CREATE POLICY "Team members can read team data"
ON public.projects
FOR SELECT
USING (
team_id IN (
SELECT team_id FROM public.team_members
WHERE user_id = auth.uid()
)
);Add write policies
Allow users to modify their own data:
CREATE POLICY "Users can update own profile"
ON public.profiles
FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);The USING clause filters which rows the user can see. The WITH CHECK clause validates the data being written.
Verify with Flowpatrol
Run a probe to confirm RLS is working:
Run a Flowpatrol probe on https://myapp.vercel.appThe RLS check should now report no unprotected tables.
Common mistakes
Forgetting WITH CHECK
The USING clause only controls reads. Without WITH CHECK, a user could update another user's row:
-- BAD: missing WITH CHECK
CREATE POLICY "Users can update profiles"
ON public.profiles
FOR UPDATE
USING (auth.uid() = user_id);
-- GOOD: both USING and WITH CHECK
CREATE POLICY "Users can update own profile"
ON public.profiles
FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);Using service_role in client code
The service_role key bypasses all RLS. It must never appear in client-side code:
// BAD — service_role key in browser code
const supabase = createClient(url, process.env.NEXT_PUBLIC_SERVICE_KEY);
// GOOD — anon key in browser, service_role only on server
const supabase = createClient(url, process.env.NEXT_PUBLIC_ANON_KEY);Overly permissive policies
-- BAD — allows all authenticated users to read all rows
CREATE POLICY "Authenticated can read"
ON public.orders
FOR SELECT
USING (auth.role() = 'authenticated');
-- GOOD — users can only read their own orders
CREATE POLICY "Users can read own orders"
ON public.orders
FOR SELECT
USING (auth.uid() = user_id);Quick reference
-- Enable RLS
ALTER TABLE public.my_table ENABLE ROW LEVEL SECURITY;
-- Read own data
CREATE POLICY "select_own" ON public.my_table
FOR SELECT USING (auth.uid() = user_id);
-- Insert own data
CREATE POLICY "insert_own" ON public.my_table
FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Update own data
CREATE POLICY "update_own" ON public.my_table
FOR UPDATE USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- Delete own data
CREATE POLICY "delete_own" ON public.my_table
FOR DELETE USING (auth.uid() = user_id);