Control data access with PostgreSQL Row Level Security policies
Row Level Security (RLS) is PostgreSQL’s built-in authorization system that controls which rows users can access in database tables. Supabase leverages RLS to provide fine-grained access control.
-- 1. Create tablecreate table profiles ( id uuid primary key references auth.users(id), username text, bio text, updated_at timestamp with time zone);-- 2. Enable RLSalter table profiles enable row level security;-- 3. Users can view their own profilecreate policy "Users can view their own profile"on profilesfor selectusing ( (select auth.uid()) = id );-- 4. Users can update their own profilecreate policy "Users can update their own profile"on profilesfor updateusing ( (select auth.uid()) = id );-- 5. Users can insert their own profilecreate policy "Users can insert their own profile"on profilesfor insertwith check ( (select auth.uid()) = id );
-- Teams tablecreate table teams ( id uuid primary key default gen_random_uuid(), name text not null);-- Team members junction tablecreate table team_members ( team_id uuid references teams(id), user_id uuid references auth.users(id), role text, primary key (team_id, user_id));-- Projects tablecreate table projects ( id uuid primary key default gen_random_uuid(), team_id uuid references teams(id), name text, description text);alter table teams enable row level security;alter table team_members enable row level security;alter table projects enable row level security;-- Team members can view their teamscreate policy "Users can view teams they belong to"on teamsfor selectusing ( id in ( select team_id from team_members where user_id = (select auth.uid()) ));-- Team members can view projectscreate policy "Team members can view team projects"on projectsfor selectusing ( team_id in ( select team_id from team_members where user_id = (select auth.uid()) ));-- Only team admins can update projectscreate policy "Team admins can update projects"on projectsfor updateusing ( team_id in ( select team_id from team_members where user_id = (select auth.uid()) and role = 'admin' ));
-- Users can view their own data and data of users they managecreate policy "Managers can view subordinate data"on employee_datafor selectusing ( user_id = (select auth.uid()) or manager_id = (select auth.uid()));
create policy "Team members can update team details"on teamsfor updateusing ( (select auth.uid()) in ( select user_id from team_members where team_id = id ));
create policy "Complex access control"on documentsfor selectusing ( -- Owner can always access owner_id = (select auth.uid()) -- OR document is public or is_public = true -- OR user is in shared_with list or (select auth.uid()) = any(shared_with) -- OR user is team member and doc is team-visible or ( team_visible = true and team_id in ( select team_id from team_members where user_id = (select auth.uid()) ) ));
Create functions that run with elevated privileges:
create or replace function get_user_stats()returns jsonlanguage plpgsqlsecurity definer -- Run with function creator's privilegesset search_path = publicas $$declare result json;begin select json_build_object( 'total_users', count(*), 'active_users', count(*) filter (where last_seen > now() - interval '7 days') ) into result from auth.users; return result;end;$$;
-- Switch to specific user for testingset local role authenticated;set local request.jwt.claims.sub = 'user-uuid-here';-- Test your queriesselect * from profiles;-- Resetreset role;
// Sign in as test userconst { data: { user } } = await supabase.auth.signInWithPassword({ email: 'test@example.com', password: 'password',})// Test RLS policiesconst { data, error } = await supabase .from('profiles') .select('*')// Should only return data allowed by RLS policiesconsole.log(data)
-- List all policies on a tableselect schemaname, tablename, policyname, permissive, roles, cmd, qual, with_checkfrom pg_policieswhere tablename = 'profiles';
Enable query logging to see RLS in action:
-- Show query plans (admin only)explain (analyze, verbose, buffers)select * from profiles;