[{"data":1,"prerenderedAt":279},["ShallowReactive",2],{"blog-how-to/supabase-rls-policies":3},{"id":4,"title":5,"body":6,"category":259,"date":260,"dateModified":261,"description":262,"draft":263,"extension":264,"faq":265,"featured":263,"headerVariant":266,"image":265,"keywords":265,"meta":267,"navigation":268,"ogDescription":269,"ogTitle":265,"path":270,"readTime":265,"schemaOrg":271,"schemaType":272,"seo":273,"sitemap":274,"stem":275,"tags":276,"twitterCard":277,"__hash__":278},"blog/blog/how-to/supabase-rls-policies.md","How to Write Supabase RLS Policies",{"type":7,"value":8,"toc":249},"minimark",[9,13,17,21,27,32,43,88,92,95,101,105,108,114,118,121,127,131,134,140,146,150,153,159,175,179,185,210,230],[10,11],"category-badge",{"category":12},"How-To Guide",[14,15,5],"h1",{"id":16},"how-to-write-supabase-rls-policies",[18,19,20],"p",{},"Real-world examples for common scenarios",[22,23,24],"tldr",{},[18,25,26],{},"TL;DR:\nUse\nauth.uid()\nto get the current user's ID. Write separate policies for SELECT, INSERT, UPDATE, and DELETE. Use\nUSING\nfor read operations and\nWITH CHECK\nfor write operations. Test every policy before deploying.",[28,29,31],"h2",{"id":30},"policy-syntax-basics","Policy Syntax Basics",[33,34,39],"pre",{"className":35,"code":37,"language":38},[36],"language-text","CREATE POLICY \"policy_name\"\nON table_name\nFOR operation  -- SELECT, INSERT, UPDATE, DELETE, or ALL\nTO role        -- Usually 'authenticated' or 'anon'\nUSING (expression)      -- For SELECT, UPDATE, DELETE (existing rows)\nWITH CHECK (expression) -- For INSERT, UPDATE (new data)\n","text",[40,41,37],"code",{"__ignoreMap":42},"",[44,45,46,62],"table",{},[47,48,49],"thead",{},[50,51,52,56,59],"tr",{},[53,54,55],"th",{},"Clause",[53,57,58],{},"Used For",[53,60,61],{},"Checks",[63,64,65,77],"tbody",{},[50,66,67,71,74],{},[68,69,70],"td",{},"USING",[68,72,73],{},"SELECT, UPDATE, DELETE",[68,75,76],{},"Which existing rows can be accessed",[50,78,79,82,85],{},[68,80,81],{},"WITH CHECK",[68,83,84],{},"INSERT, UPDATE",[68,86,87],{},"What data can be written",[28,89,91],{"id":90},"pattern-1-user-owned-data","Pattern 1: User-Owned Data",[18,93,94],{},"Most common pattern: users can only access their own data.",[33,96,99],{"className":97,"code":98,"language":38},[36],"-- Table structure\nCREATE TABLE todos (\n  id uuid DEFAULT gen_random_uuid() PRIMARY KEY,\n  user_id uuid REFERENCES auth.users(id) NOT NULL,\n  title text NOT NULL,\n  completed boolean DEFAULT false,\n  created_at timestamp with time zone DEFAULT now()\n);\n\n-- Enable RLS\nALTER TABLE todos ENABLE ROW LEVEL SECURITY;\n\n-- Users can view their own todos\nCREATE POLICY \"Users can view own todos\"\nON todos FOR SELECT\nTO authenticated\nUSING (auth.uid() = user_id);\n\n-- Users can create their own todos\nCREATE POLICY \"Users can create own todos\"\nON todos FOR INSERT\nTO authenticated\nWITH CHECK (auth.uid() = user_id);\n\n-- Users can update their own todos\nCREATE POLICY \"Users can update own todos\"\nON todos FOR UPDATE\nTO authenticated\nUSING (auth.uid() = user_id);\n\n-- Users can delete their own todos\nCREATE POLICY \"Users can delete own todos\"\nON todos FOR DELETE\nTO authenticated\nUSING (auth.uid() = user_id);\n",[40,100,98],{"__ignoreMap":42},[28,102,104],{"id":103},"pattern-2-public-profiles","Pattern 2: Public Profiles",[18,106,107],{},"Anyone can view profiles, but only owners can edit.",[33,109,112],{"className":110,"code":111,"language":38},[36],"-- Table structure\nCREATE TABLE profiles (\n  id uuid REFERENCES auth.users(id) PRIMARY KEY,\n  username text UNIQUE,\n  avatar_url text,\n  bio text,\n  updated_at timestamp with time zone DEFAULT now()\n);\n\nALTER TABLE profiles ENABLE ROW LEVEL SECURITY;\n\n-- Anyone can view profiles\nCREATE POLICY \"Public profiles are viewable\"\nON profiles FOR SELECT\nTO anon, authenticated\nUSING (true);\n\n-- Users can update their own profile\nCREATE POLICY \"Users can update own profile\"\nON profiles FOR UPDATE\nTO authenticated\nUSING (auth.uid() = id)\nWITH CHECK (auth.uid() = id);\n\n-- Users can insert their own profile (on signup)\nCREATE POLICY \"Users can insert own profile\"\nON profiles FOR INSERT\nTO authenticated\nWITH CHECK (auth.uid() = id);\n",[40,113,111],{"__ignoreMap":42},[28,115,117],{"id":116},"pattern-3-published-vs-draft-content","Pattern 3: Published vs Draft Content",[18,119,120],{},"Public can see published content, authors can see their drafts.",[33,122,125],{"className":123,"code":124,"language":38},[36],"CREATE TABLE posts (\n  id uuid DEFAULT gen_random_uuid() PRIMARY KEY,\n  author_id uuid REFERENCES auth.users(id) NOT NULL,\n  title text NOT NULL,\n  content text,\n  status text DEFAULT 'draft' CHECK (status IN ('draft', 'published')),\n  created_at timestamp with time zone DEFAULT now()\n);\n\nALTER TABLE posts ENABLE ROW LEVEL SECURITY;\n\n-- Anyone can view published posts\nCREATE POLICY \"Published posts are public\"\nON posts FOR SELECT\nTO anon, authenticated\nUSING (status = 'published');\n\n-- Authors can view their own posts (including drafts)\nCREATE POLICY \"Authors can view own posts\"\nON posts FOR SELECT\nTO authenticated\nUSING (auth.uid() = author_id);\n\n-- Authors can create posts\nCREATE POLICY \"Authors can create posts\"\nON posts FOR INSERT\nTO authenticated\nWITH CHECK (auth.uid() = author_id);\n\n-- Authors can update their posts\nCREATE POLICY \"Authors can update own posts\"\nON posts FOR UPDATE\nTO authenticated\nUSING (auth.uid() = author_id);\n\n-- Authors can delete their posts\nCREATE POLICY \"Authors can delete own posts\"\nON posts FOR DELETE\nTO authenticated\nUSING (auth.uid() = author_id);\n",[40,126,124],{"__ignoreMap":42},[28,128,130],{"id":129},"pattern-4-teamorganization-access","Pattern 4: Team/Organization Access",[18,132,133],{},"Users can access data for teams they belong to.",[33,135,138],{"className":136,"code":137,"language":38},[36],"-- Team membership table\nCREATE TABLE team_members (\n  team_id uuid REFERENCES teams(id),\n  user_id uuid REFERENCES auth.users(id),\n  role text DEFAULT 'member' CHECK (role IN ('owner', 'admin', 'member')),\n  PRIMARY KEY (team_id, user_id)\n);\n\n-- Team projects\nCREATE TABLE projects (\n  id uuid DEFAULT gen_random_uuid() PRIMARY KEY,\n  team_id uuid REFERENCES teams(id) NOT NULL,\n  name text NOT NULL,\n  created_at timestamp with time zone DEFAULT now()\n);\n\nALTER TABLE projects ENABLE ROW LEVEL SECURITY;\n\n-- Helper function to check team membership\nCREATE OR REPLACE FUNCTION is_team_member(team_id uuid)\nRETURNS boolean AS $$\n  SELECT EXISTS (\n    SELECT 1 FROM team_members\n    WHERE team_members.team_id = $1\n    AND team_members.user_id = auth.uid()\n  );\n$$ LANGUAGE sql SECURITY DEFINER;\n\n-- Team members can view team projects\nCREATE POLICY \"Team members can view projects\"\nON projects FOR SELECT\nTO authenticated\nUSING (is_team_member(team_id));\n\n-- Only admins/owners can create projects\nCREATE OR REPLACE FUNCTION is_team_admin(team_id uuid)\nRETURNS boolean AS $$\n  SELECT EXISTS (\n    SELECT 1 FROM team_members\n    WHERE team_members.team_id = $1\n    AND team_members.user_id = auth.uid()\n    AND team_members.role IN ('owner', 'admin')\n  );\n$$ LANGUAGE sql SECURITY DEFINER;\n\nCREATE POLICY \"Admins can create projects\"\nON projects FOR INSERT\nTO authenticated\nWITH CHECK (is_team_admin(team_id));\n",[40,139,137],{"__ignoreMap":42},[141,142,143],"tip-box",{},[18,144,145],{},"Use helper functions:\nFor complex checks, create reusable SQL functions. This keeps your policies readable and makes updates easier.",[28,147,149],{"id":148},"pattern-5-private-messages","Pattern 5: Private Messages",[18,151,152],{},"Users can see messages they sent or received.",[33,154,157],{"className":155,"code":156,"language":38},[36],"CREATE TABLE messages (\n  id uuid DEFAULT gen_random_uuid() PRIMARY KEY,\n  sender_id uuid REFERENCES auth.users(id) NOT NULL,\n  recipient_id uuid REFERENCES auth.users(id) NOT NULL,\n  content text NOT NULL,\n  read boolean DEFAULT false,\n  created_at timestamp with time zone DEFAULT now()\n);\n\nALTER TABLE messages ENABLE ROW LEVEL SECURITY;\n\n-- Users can view messages they sent or received\nCREATE POLICY \"Users can view own messages\"\nON messages FOR SELECT\nTO authenticated\nUSING (\n  auth.uid() = sender_id OR auth.uid() = recipient_id\n);\n\n-- Users can only send messages as themselves\nCREATE POLICY \"Users can send messages\"\nON messages FOR INSERT\nTO authenticated\nWITH CHECK (auth.uid() = sender_id);\n\n-- Recipients can mark messages as read\nCREATE POLICY \"Recipients can update read status\"\nON messages FOR UPDATE\nTO authenticated\nUSING (auth.uid() = recipient_id)\nWITH CHECK (auth.uid() = recipient_id);\n",[40,158,156],{"__ignoreMap":42},[160,161,162,165],"warning-box",{},[18,163,164],{},"Common Mistake: Forgetting INSERT Policies",[18,166,167,168,170,171,174],{},"Without an INSERT policy with ",[40,169,81],{},", users might be able to insert data claiming to be another user. Always verify ",[40,172,173],{},"auth.uid()"," matches the user_id being inserted.",[28,176,178],{"id":177},"testing-your-policies","Testing Your Policies",[33,180,183],{"className":181,"code":182,"language":38},[36],"-- Test as a specific user in SQL Editor\nSET request.jwt.claims.sub = 'user-uuid-here';\n\n-- Try to select data\nSELECT * FROM todos;\n\n-- Try to access another user's data (should fail or return empty)\nSELECT * FROM todos WHERE user_id = 'other-user-uuid';\n\n-- Reset\nRESET request.jwt.claims.sub;\n",[40,184,182],{"__ignoreMap":42},[186,187,188,195,204],"faq-section",{},[189,190,192],"faq-item",{"question":191},"Do I need policies for the service_role key?",[18,193,194],{},"No, the service_role key bypasses RLS entirely. That's why it must stay secret and only be used server-side. RLS policies only apply to the anon and authenticated roles.",[189,196,198],{"question":197},"Why does USING (true) expose all data?",[18,199,200,203],{},[40,201,202],{},"USING (true)"," means \"all rows pass this check.\" It's appropriate for public data (like published posts) but dangerous if you meant to restrict access.",[189,205,207],{"question":206},"Can I use JOINs in RLS policies?",[18,208,209],{},"Yes, but be careful about performance. Complex JOINs in policies run for every row access. Consider using helper functions with proper indexes, or denormalize data if needed.",[18,211,212,216,221,222,221,226],{},[213,214,215],"strong",{},"Related guides:",[217,218,220],"a",{"href":219},"/blog/how-to/setup-supabase-rls","How to Set Up Supabase RLS"," ·\n",[217,223,225],{"href":224},"/blog/how-to/test-supabase-rls","How to Test Supabase RLS",[217,227,229],{"href":228},"/blog/guides/supabase","Supabase Security Guide",[231,232,233,239,244],"related-articles",{},[234,235],"related-card",{"description":236,"href":237,"title":238},"Complete guide to secure Auth0 setup. Configure applications, handle callbacks safely, validate tokens, implement author","/blog/how-to/auth0-basics","How to Set Up Auth0 Securely",[234,240],{"description":241,"href":242,"title":243},"Step-by-step guide to storing and retrieving secrets with AWS Secrets Manager. Secure your API keys, database credential","/blog/how-to/aws-secrets-manager","How to Use AWS Secrets Manager",[234,245],{"description":246,"href":247,"title":248},"Step-by-step guide to SSL certificate renewal. Set up automatic renewal with Certbot, monitor expiration dates, and trou","/blog/how-to/certificate-renewal","How to Handle SSL Certificate Renewal",{"title":42,"searchDepth":250,"depth":250,"links":251},2,[252,253,254,255,256,257,258],{"id":30,"depth":250,"text":31},{"id":90,"depth":250,"text":91},{"id":103,"depth":250,"text":104},{"id":116,"depth":250,"text":117},{"id":129,"depth":250,"text":130},{"id":148,"depth":250,"text":149},{"id":177,"depth":250,"text":178},"how-to","2026-01-28","2026-02-13","Learn to write effective Row Level Security policies in Supabase. Real examples for profiles, posts, teams, and multi-tenant apps with step-by-step explanations.",false,"md",null,"yellow",{},true,"Real examples for Row Level Security policies in Supabase.","/blog/how-to/supabase-rls-policies","[object Object]","HowTo",{"title":5,"description":262},{"loc":270},"blog/how-to/supabase-rls-policies",[],"summary_large_image","GIltRiEuKWYl2QOP7ydpQkxNDvZ5dKPdU386g7W678o",1775843927093]