Row Level Security (RLS) is Supabase's database-layer access control, and it's the single most important security primitive in your stack. When misconfigured, it exposes data; when done right, it's a bulletproof authorization layer.
The Mental Model
Think of RLS as a WHERE clause that Postgres automatically appends to every query. A policy like `auth.uid() = user_id` means no query — regardless of your application code — can return rows belonging to another user.
The Golden Rules
**1. Enable RLS on every table, always.** An unprotected table is a security hole. Enable RLS first, add policies after.
**2. Never use `anon` key on the server.** The anon key is for client browsers only. Server-side access should use the service role key, carefully, with full awareness that it bypasses RLS.
**3. Test with the user's perspective.** After writing policies, switch to Supabase's Table Editor, set the user to a test user, and verify you see exactly what you expect.
Common Patterns
-- Users can only see their own data
CREATE POLICY "own_data" ON public.documents
FOR ALL USING (auth.uid() = user_id);-- Team members can see shared resources CREATE POLICY "team_access" ON public.projects FOR SELECT USING ( EXISTS ( SELECT 1 FROM team_members WHERE team_id = projects.team_id AND user_id = auth.uid() ) ); ```
The Pitfall to Avoid
Recursive policies — policies that query tables which themselves have RLS — can cause infinite loops. Use security definer functions to break the cycle.
RLS is not optional. Treat it as load-bearing infrastructure, not an afterthought.