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 table

Enabling 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.app

The 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);