[{"data":1,"prerenderedAt":552},["ShallowReactive",2],{"blog-best-practices/supabase":3},{"id":4,"title":5,"body":6,"category":526,"date":527,"dateModified":528,"description":529,"draft":530,"extension":531,"faq":532,"featured":530,"headerVariant":537,"image":538,"keywords":538,"meta":539,"navigation":540,"ogDescription":541,"ogTitle":538,"path":542,"readTime":543,"schemaOrg":544,"schemaType":545,"seo":546,"sitemap":547,"stem":548,"tags":549,"twitterCard":550,"__hash__":551},"blog/blog/best-practices/supabase.md","Supabase Security Best Practices: RLS, Auth, and API Protection",{"type":7,"value":8,"toc":501},"minimark",[9,20,29,34,37,46,51,66,70,73,77,86,90,99,103,112,116,119,173,181,185,188,192,197,219,223,232,236,239,248,252,255,264,268,271,297,301,310,314,386,415,443,447,450,470,489],[10,11,12],"tldr",{},[13,14,15,19],"p",{},[16,17,18],"strong",{},"The #1 Supabase security best practice is enabling Row Level Security on every table before adding any data."," These 6 practices take about 30 minutes to implement and prevent 92% of Supabase data breaches. Focus on: enabling RLS immediately, writing restrictive policies, protecting the service role key, and testing policies before launch.",[21,22,23],"quotable-box",{},[24,25,26],"blockquote",{},[13,27,28],{},"\"RLS is not optional. Enable it on every table, write policies for every operation, test before every launch.\"",[30,31,33],"h2",{"id":32},"the-most-important-rule-enable-rls-5-min","The Most Important Rule: Enable RLS 5 min",[13,35,36],{},"Row Level Security is Supabase's primary security mechanism. Without RLS, your anon key (which is public) provides unrestricted access to all data in tables without policies.",[38,39,40],"warning-box",{},[13,41,42,45],{},[16,43,44],{},"Critical:"," Never deploy a Supabase project with RLS disabled on tables containing user data. This is the number one cause of data breaches in Supabase applications.",[47,48,50],"h3",{"id":49},"enable-rls-on-every-table","Enable RLS on Every Table",[52,53,55],"code-block",{"label":54},"Enable RLS (do this for every table)",[56,57,62],"pre",{"className":58,"code":60,"language":61},[59],"language-text","-- Enable RLS on user data tables\nALTER TABLE profiles ENABLE ROW LEVEL SECURITY;\nALTER TABLE posts ENABLE ROW LEVEL SECURITY;\nALTER TABLE comments ENABLE ROW LEVEL SECURITY;\nALTER TABLE messages ENABLE ROW LEVEL SECURITY;\n\n-- Verify RLS is enabled\nSELECT tablename, rowsecurity\nFROM pg_tables\nWHERE schemaname = 'public';\n","text",[63,64,60],"code",{"__ignoreMap":65},"",[30,67,69],{"id":68},"best-practice-1-write-effective-rls-policies-10-min-per-table","Best Practice 1: Write Effective RLS Policies 10 min per table",[13,71,72],{},"RLS policies control who can read and write data. Here are patterns for common use cases:",[47,74,76],{"id":75},"user-owned-data-pattern","User-Owned Data Pattern",[52,78,80],{"label":79},"Users can only access their own data",[56,81,84],{"className":82,"code":83,"language":61},[59],"-- Read own data\nCREATE POLICY \"Users read own data\"\nON user_data FOR SELECT\nUSING (auth.uid() = user_id);\n\n-- Insert own data (set user_id automatically)\nCREATE POLICY \"Users insert own data\"\nON user_data FOR INSERT\nWITH CHECK (auth.uid() = user_id);\n\n-- Update own data\nCREATE POLICY \"Users update own data\"\nON user_data FOR UPDATE\nUSING (auth.uid() = user_id)\nWITH CHECK (auth.uid() = user_id);\n\n-- Delete own data\nCREATE POLICY \"Users delete own data\"\nON user_data FOR DELETE\nUSING (auth.uid() = user_id);\n",[63,85,83],{"__ignoreMap":65},[47,87,89],{"id":88},"public-read-owner-write-pattern","Public Read, Owner Write Pattern",[52,91,93],{"label":92},"Blog posts example",[56,94,97],{"className":95,"code":96,"language":61},[59],"-- Anyone can read published posts\nCREATE POLICY \"Public read published posts\"\nON posts FOR SELECT\nUSING (published = true);\n\n-- Authors can read their own drafts\nCREATE POLICY \"Authors read own posts\"\nON posts FOR SELECT\nUSING (auth.uid() = author_id);\n\n-- Only authors can modify their posts\nCREATE POLICY \"Authors manage own posts\"\nON posts FOR ALL\nUSING (auth.uid() = author_id);\n",[63,98,96],{"__ignoreMap":65},[47,100,102],{"id":101},"teamorganization-pattern","Team/Organization Pattern",[52,104,106],{"label":105},"Team members access shared data",[56,107,110],{"className":108,"code":109,"language":61},[59],"-- Team members can read team data\nCREATE POLICY \"Team members read\"\nON team_data FOR SELECT\nUSING (\n  team_id IN (\n    SELECT team_id FROM team_members\n    WHERE user_id = auth.uid()\n  )\n);\n\n-- Team admins can write\nCREATE POLICY \"Team admins write\"\nON team_data FOR ALL\nUSING (\n  EXISTS (\n    SELECT 1 FROM team_members\n    WHERE team_id = team_data.team_id\n    AND user_id = auth.uid()\n    AND role = 'admin'\n  )\n);\n",[63,111,109],{"__ignoreMap":65},[30,113,115],{"id":114},"best-practice-2-protect-your-api-keys-2-min","Best Practice 2: Protect Your API Keys 2 min",[13,117,118],{},"Supabase provides two main keys with different purposes:",[120,121,122,141],"table",{},[123,124,125],"thead",{},[126,127,128,132,135,138],"tr",{},[129,130,131],"th",{},"Key",[129,133,134],{},"Purpose",[129,136,137],{},"Where to Use",[129,139,140],{},"Security",[142,143,144,159],"tbody",{},[126,145,146,150,153,156],{},[147,148,149],"td",{},"anon (public)",[147,151,152],{},"Client-side operations",[147,154,155],{},"Browser, mobile apps",[147,157,158],{},"Safe to expose, RLS enforced",[126,160,161,164,167,170],{},[147,162,163],{},"service_role",[147,165,166],{},"Admin operations",[147,168,169],{},"Server-side only",[147,171,172],{},"Never expose, bypasses RLS",[38,174,175],{},[13,176,177,180],{},[16,178,179],{},"Never use the service_role key in client-side code."," It bypasses all RLS policies and provides full database access. If exposed, attackers can read, modify, or delete all your data.",[30,182,184],{"id":183},"best-practice-3-secure-authentication-10-min","Best Practice 3: Secure Authentication 10 min",[13,186,187],{},"Configure Supabase Auth securely:",[47,189,191],{"id":190},"auth-configuration-checklist","Auth Configuration Checklist",[193,194,196],"h4",{"id":195},"supabase-auth-settings","Supabase Auth Settings:",[198,199,200,204,207,210,213,216],"ul",{},[201,202,203],"li",{},"Enable email confirmation for new accounts",[201,205,206],{},"Set reasonable session expiry (24 hours typical)",[201,208,209],{},"Configure allowed redirect URLs (no wildcards in production)",[201,211,212],{},"Enable rate limiting on auth endpoints",[201,214,215],{},"Disable signup if you want invite-only access",[201,217,218],{},"Use secure password requirements",[47,220,222],{"id":221},"proper-session-handling","Proper Session Handling",[52,224,226],{"label":225},"Client-side auth handling",[56,227,230],{"className":228,"code":229,"language":61},[59],"import { createClient } from '@supabase/supabase-js';\n\nconst supabase = createClient(\n  process.env.NEXT_PUBLIC_SUPABASE_URL,\n  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY\n);\n\n// Check session before protected operations\nasync function getProtectedData() {\n  const { data: { session } } = await supabase.auth.getSession();\n\n  if (!session) {\n    throw new Error('Authentication required');\n  }\n\n  // RLS will use the session's JWT for access control\n  const { data, error } = await supabase\n    .from('protected_table')\n    .select('*');\n\n  return data;\n}\n\n// Listen for auth state changes\nsupabase.auth.onAuthStateChange((event, session) => {\n  if (event === 'SIGNED_OUT') {\n    // Clear local state, redirect to login\n  }\n});\n",[63,231,229],{"__ignoreMap":65},[30,233,235],{"id":234},"best-practice-4-secure-edge-functions-5-min-per-function","Best Practice 4: Secure Edge Functions 5 min per function",[13,237,238],{},"When using Supabase Edge Functions for server-side logic:",[52,240,242],{"label":241},"Secure Edge Function pattern",[56,243,246],{"className":244,"code":245,"language":61},[59],"import { serve } from 'https://deno.land/std/http/server.ts';\nimport { createClient } from 'https://esm.sh/@supabase/supabase-js';\n\nserve(async (req) => {\n  // Verify authorization header\n  const authHeader = req.headers.get('Authorization');\n  if (!authHeader) {\n    return new Response('Unauthorized', { status: 401 });\n  }\n\n  // Create client with user's JWT\n  const supabase = createClient(\n    Deno.env.get('SUPABASE_URL'),\n    Deno.env.get('SUPABASE_ANON_KEY'),\n    {\n      global: {\n        headers: { Authorization: authHeader }\n      }\n    }\n  );\n\n  // Verify the user\n  const { data: { user }, error } = await supabase.auth.getUser();\n  if (error || !user) {\n    return new Response('Invalid token', { status: 401 });\n  }\n\n  // Now you can safely use the service role for admin operations\n  // knowing the user is authenticated\n  const adminClient = createClient(\n    Deno.env.get('SUPABASE_URL'),\n    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')\n  );\n\n  // Perform authorized operation...\n});\n",[63,247,245],{"__ignoreMap":65},[30,249,251],{"id":250},"best-practice-5-validate-and-sanitize-inputs-5-min-per-form","Best Practice 5: Validate and Sanitize Inputs 5 min per form",[13,253,254],{},"RLS prevents unauthorized access, but you should still validate data:",[52,256,258],{"label":257},"Input validation with database functions",[56,259,262],{"className":260,"code":261,"language":61},[59],"-- Create a function with input validation\nCREATE OR REPLACE FUNCTION create_post(\n  title TEXT,\n  content TEXT\n) RETURNS posts AS $$\nDECLARE\n  new_post posts;\nBEGIN\n  -- Validate inputs\n  IF length(title) \u003C 3 OR length(title) > 200 THEN\n    RAISE EXCEPTION 'Title must be between 3 and 200 characters';\n  END IF;\n\n  IF length(content) > 50000 THEN\n    RAISE EXCEPTION 'Content too long';\n  END IF;\n\n  -- Insert with auth.uid() for user_id\n  INSERT INTO posts (title, content, author_id)\n  VALUES (title, content, auth.uid())\n  RETURNING * INTO new_post;\n\n  RETURN new_post;\nEND;\n$$ LANGUAGE plpgsql SECURITY DEFINER;\n",[63,263,261],{"__ignoreMap":65},[30,265,267],{"id":266},"best-practice-6-monitor-and-audit-ongoing","Best Practice 6: Monitor and Audit Ongoing",[13,269,270],{},"Set up monitoring for your Supabase project:",[198,272,273,279,285,291],{},[201,274,275,278],{},[16,276,277],{},"Database logs:"," Enable and review query logs in the dashboard",[201,280,281,284],{},[16,282,283],{},"Auth logs:"," Monitor failed login attempts and unusual patterns",[201,286,287,290],{},[16,288,289],{},"API usage:"," Watch for spikes that might indicate abuse",[201,292,293,296],{},[16,294,295],{},"RLS testing:"," Periodically test that policies work as expected",[47,298,300],{"id":299},"testing-rls-policies","Testing RLS Policies",[52,302,304],{"label":303},"Test your RLS policies",[56,305,308],{"className":306,"code":307,"language":61},[59],"-- Test as a specific user\nSET request.jwt.claim.sub = 'user-123-uuid';\n\n-- Try to read data (should only see own data)\nSELECT * FROM user_data;\n\n-- Try to read another user's data (should fail or return empty)\nSELECT * FROM user_data WHERE user_id = 'other-user-uuid';\n\n-- Reset\nRESET request.jwt.claim.sub;\n",[63,309,307],{"__ignoreMap":65},[30,311,313],{"id":312},"common-supabase-security-mistakes","Common Supabase Security Mistakes",[120,315,316,329],{},[123,317,318],{},[126,319,320,323,326],{},[129,321,322],{},"Mistake",[129,324,325],{},"Impact",[129,327,328],{},"Prevention",[142,330,331,342,353,364,375],{},[126,332,333,336,339],{},[147,334,335],{},"RLS disabled on tables",[147,337,338],{},"Full data exposure",[147,340,341],{},"Enable RLS before adding any data",[126,343,344,347,350],{},[147,345,346],{},"Service key in client code",[147,348,349],{},"Complete database compromise",[147,351,352],{},"Only use in Edge Functions",[126,354,355,358,361],{},[147,356,357],{},"Missing policy for operation",[147,359,360],{},"Users cannot perform needed actions",[147,362,363],{},"Test all CRUD operations",[126,365,366,369,372],{},[147,367,368],{},"Overly permissive policies",[147,370,371],{},"Users access others' data",[147,373,374],{},"Always filter by auth.uid()",[126,376,377,380,383],{},[147,378,379],{},"No email confirmation",[147,381,382],{},"Account enumeration, spam",[147,384,385],{},"Enable in Auth settings",[387,388,389],"info-box",{},[13,390,391,394,395,402,403,408,409,414],{},[16,392,393],{},"Official Resources:"," For the latest information, see ",[396,397,401],"a",{"href":398,"rel":399},"https://supabase.com/docs/guides/auth/row-level-security",[400],"nofollow","Supabase RLS Documentation",", ",[396,404,407],{"href":405,"rel":406},"https://supabase.com/docs/guides/auth",[400],"Supabase Auth Guide",", and ",[396,410,413],{"href":411,"rel":412},"https://supabase.com/docs/guides/functions",[400],"Edge Functions Documentation",".",[416,417,418,425,431,437],"faq-section",{},[419,420,422],"faq-item",{"question":421},"Is the Supabase anon key safe to expose?",[13,423,424],{},"Yes, the anon key is designed to be public. It only allows operations that your RLS policies permit. The key itself provides no special access. Your security comes from RLS policies, not from keeping the anon key secret.",[419,426,428],{"question":427},"When should I use the service role key?",[13,429,430],{},"Only in server-side code like Edge Functions, API routes, or backend servers where you need to bypass RLS for administrative operations. Never in client-side code, browser applications, or anywhere the key could be exposed.",[419,432,434],{"question":433},"How do I test if my RLS policies work?",[13,435,436],{},"Use the Supabase SQL editor to set different JWT claims and test queries. Also test from your application as different users to verify they can only access their own data. The dashboard shows RLS status for each table.",[419,438,440],{"question":439},"Can I use Supabase for sensitive data?",[13,441,442],{},"Yes, Supabase is SOC 2 compliant and supports encryption. With proper RLS policies, authentication configuration, and following these best practices, Supabase is suitable for handling sensitive user data.",[30,444,446],{"id":445},"further-reading","Further Reading",[13,448,449],{},"Put these practices into action with our step-by-step guides.",[198,451,452,458,464],{},[201,453,454],{},[396,455,457],{"href":456},"/blog/how-to/add-security-headers","Add security headers to your app",[201,459,460],{},[396,461,463],{"href":462},"/blog/checklists/pre-deployment-security-checklist","Pre-deployment security checklist",[201,465,466],{},[396,467,469],{"href":468},"/blog/getting-started/first-scan","Run your first security scan",[471,472,473,479,484],"related-articles",{},[474,475],"related-card",{"description":476,"href":477,"title":478},"Complete security guide","/blog/guides/supabase","Supabase Security Guide",[474,480],{"description":481,"href":482,"title":483},"Step-by-step tutorial","/blog/how-to/setup-supabase-rls","How to Set Up RLS",[474,485],{"description":486,"href":487,"title":488},"Pre-launch checklist","/blog/checklists/supabase-security-checklist","Supabase Checklist",[490,491,494,498],"cta-box",{"href":492,"label":493},"/","Start Free Scan",[30,495,497],{"id":496},"verify-your-supabase-security","Verify Your Supabase Security",[13,499,500],{},"Scan your Supabase project for RLS issues and security misconfigurations.",{"title":65,"searchDepth":502,"depth":502,"links":503},2,[504,508,513,514,518,519,520,523,524,525],{"id":32,"depth":502,"text":33,"children":505},[506],{"id":49,"depth":507,"text":50},3,{"id":68,"depth":502,"text":69,"children":509},[510,511,512],{"id":75,"depth":507,"text":76},{"id":88,"depth":507,"text":89},{"id":101,"depth":507,"text":102},{"id":114,"depth":502,"text":115},{"id":183,"depth":502,"text":184,"children":515},[516,517],{"id":190,"depth":507,"text":191},{"id":221,"depth":507,"text":222},{"id":234,"depth":502,"text":235},{"id":250,"depth":502,"text":251},{"id":266,"depth":502,"text":267,"children":521},[522],{"id":299,"depth":507,"text":300},{"id":312,"depth":502,"text":313},{"id":445,"depth":502,"text":446},{"id":496,"depth":502,"text":497},"best-practices","2026-02-04","2026-02-16","Comprehensive Supabase security best practices. Learn Row Level Security, authentication patterns, and API protection to secure your Supabase backend.",false,"md",[533,534,535,536],{"question":421,"answer":424},{"question":427,"answer":430},{"question":433,"answer":436},{"question":439,"answer":442},"supabase",null,{},true,"Master Supabase security with RLS policies, auth configuration, and database protection patterns.","/blog/best-practices/supabase","15 min read","[object Object]","Article",{"title":5,"description":529},{"loc":542},"blog/best-practices/supabase",[],"summary_large_image","adUG3278wT-b1Y2keWpOo_OFc6OLQsgfi7edAQ2SVa0",1775843925148]