UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

1,204 lines (967 loc) 33.7 kB
--- template_name: "Supabase RLS Policies" template_version: "1.0.0" output_format: "markdown" destination: "rls-policies.md" description: "Row Level Security policies for Supabase tables" --- sections: - id: overview title: "RLS Overview" instruction: | Document the Row Level Security strategy: ## Purpose Explain the overall security model and why RLS is being used. ## Authentication Context - How users are authenticated (Supabase Auth, JWT, etc) - Available auth context variables: - `auth.uid()` - Current user ID - `auth.jwt()` - JWT claims - `auth.email()` - User email - Custom claims in JWT ## Security Model - Role-based access control (RBAC) - Multi-tenancy approach (if applicable) - Public vs authenticated vs specific role access ## Performance Considerations - RLS policy performance impact - Indexing strategy to support policies - Caching considerations ## Testing Strategy - How policies will be tested - Test users and scenarios elicit: true - id: policy-patterns title: "Common Policy Patterns" instruction: | Document reusable policy patterns used across tables: ## Pattern 1: Owner-Only Access ```sql -- Users can only access their own records (auth.uid() = user_id) ``` ## Pattern 2: Tenant-Based Access ```sql -- Users can access records in their organization (auth.uid() IN ( SELECT user_id FROM org_members WHERE org_id = table.org_id )) ``` ## Pattern 3: Role-Based Access ```sql -- Only admins can access ((auth.jwt() ->> 'role')::text = 'admin') ``` ## Pattern 4: Public Read, Authenticated Write ```sql -- SELECT: true (public read) -- INSERT/UPDATE/DELETE: auth.uid() IS NOT NULL ``` ## Pattern 5: Hierarchical Permissions ```sql -- Access based on organizational hierarchy ``` Document any other patterns specific to your application. elicit: true - id: table-policies title: "Table-by-Table Policies" instruction: | For each table requiring RLS, document comprehensive policies: # Table: `table_name` ## Enable RLS ```sql ALTER TABLE table_name ENABLE ROW LEVEL SECURITY; ``` ## SELECT Policies ### Policy: `policy_name_select` **Purpose**: Describe who can read what **Policy Expression**: ```sql CREATE POLICY "policy_name_select" ON table_name FOR SELECT TO authenticated -- or public, anon, etc USING ( -- Policy expression auth.uid() = user_id ); ``` **Rationale**: Explain the business rule **Performance**: Any indexes needed to support this policy ## INSERT Policies ### Policy: `policy_name_insert` **Purpose**: Describe who can create records **Policy Expression**: ```sql CREATE POLICY "policy_name_insert" ON table_name FOR INSERT TO authenticated WITH CHECK ( -- Policy expression auth.uid() = user_id ); ``` **Validation**: What validations this ensures ## UPDATE Policies ### Policy: `policy_name_update` **Purpose**: Describe who can modify records **Policy Expression**: ```sql CREATE POLICY "policy_name_update" ON table_name FOR UPDATE TO authenticated USING ( -- Who can see the record to update it auth.uid() = user_id ) WITH CHECK ( -- What values they can set auth.uid() = user_id ); ``` **Notes**: USING checks old values, WITH CHECK validates new values ## DELETE Policies ### Policy: `policy_name_delete` **Purpose**: Describe who can delete records **Policy Expression**: ```sql CREATE POLICY "policy_name_delete" ON table_name FOR DELETE TO authenticated USING ( -- Who can delete auth.uid() = user_id OR (auth.jwt() ->> 'role')::text = 'admin' ); ``` ## ALL Policies (if using combined policy) Sometimes a single policy for all operations is clearer: ```sql CREATE POLICY "policy_name_all" ON table_name FOR ALL TO authenticated USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); ``` --- Repeat this section for each table. elicit: true - id: public-tables title: "Public Tables" instruction: | Document tables with public read access: # Table: `public_table_name` ## Public Read Policy ```sql ALTER TABLE public_table_name ENABLE ROW LEVEL SECURITY; CREATE POLICY "public_read_policy" ON public_table_name FOR SELECT TO anon, authenticated USING (true); ``` ## Restricted Write Policy ```sql CREATE POLICY "authenticated_write_policy" ON public_table_name FOR INSERT TO authenticated WITH CHECK (auth.uid() IS NOT NULL); ``` **Rationale**: Why this table is public **Security Considerations**: What data is safe to expose elicit: false - id: service-role title: "Service Role Bypass" instruction: | Document scenarios where service role (bypass RLS) is used: ## Service Role Usage ### Backend Operations Operations that need to bypass RLS: - Scheduled jobs (cron, edge functions with service key) - Admin operations - Data migration - Analytics aggregation ### Safety Measures - How service key is secured - Where service role operations are logged - Who has access to service key ### Alternatives When possible, prefer: - Security definer functions with RLS - Elevated permission policies - Temporary privilege escalation elicit: false - id: helper-functions title: "Security Helper Functions" instruction: | Document PostgreSQL functions that support RLS: ## Function: `check_user_permission` ```sql CREATE OR REPLACE FUNCTION check_user_permission( user_id uuid, resource_id uuid, permission_type text ) RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER AS $$ BEGIN -- Permission checking logic RETURN EXISTS ( SELECT 1 FROM permissions WHERE user_id = $1 AND resource_id = $2 AND permission = $3 ); END; $$; ``` **Usage in Policies**: ```sql USING (check_user_permission(auth.uid(), id, 'read')) ``` ## Function: `get_user_org_id` ```sql CREATE OR REPLACE FUNCTION get_user_org_id() RETURNS uuid LANGUAGE sql STABLE AS $$ SELECT org_id FROM user_profiles WHERE user_id = auth.uid(); $$; ``` **Usage in Policies**: ```sql USING (org_id = get_user_org_id()) ``` Document all helper functions used in RLS policies. elicit: false - id: multi-tenancy title: "Multi-Tenancy Implementation" condition: "multi_tenant" instruction: | If implementing multi-tenancy with RLS: ## Tenant Isolation Strategy ### Tenant Identification - How tenants are identified (org_id, team_id, etc) - Where tenant ID is stored (JWT claim, database lookup) ### Tenant-Scoped Tables For each tenant-scoped table: ```sql -- Example: projects table CREATE POLICY "tenant_isolation_policy" ON projects FOR ALL TO authenticated USING ( org_id = (auth.jwt() ->> 'org_id')::uuid ) WITH CHECK ( org_id = (auth.jwt() ->> 'org_id')::uuid ); ``` ### Cross-Tenant Scenarios - Shared resources across tenants - Super admin access - Tenant-to-tenant relationships ### Performance - Indexes on tenant_id columns - Query patterns that leverage tenant isolation elicit: true - id: storage-policies title: "Supabase Storage Policies" instruction: | RLS policies for Supabase Storage buckets (storage.objects table). ## Overview Supabase Storage uses RLS on the `storage.objects` system table to control file access. By default, Storage requires explicit RLS policies - no uploads allowed without policies. ## Pattern 1: User-Specific Uploads Users can only upload files to their own folder: ```sql CREATE POLICY "Users upload own avatars" ON storage.objects FOR INSERT TO authenticated WITH CHECK ( bucket_id = 'avatars' AND (select auth.uid())::text = (storage.foldername(name))[1] ); ``` **Folder Structure**: `avatars/{user_id}/filename.jpg` **How it works**: - `storage.foldername(name)` splits path by `/` returning array - `[1]` gets first folder (user_id) - Compares with authenticated user's ID ## Pattern 2: Public Read, Authenticated Write Anyone can view files, only authenticated users can upload: ```sql -- Public read CREATE POLICY "Public avatars readable" ON storage.objects FOR SELECT TO public USING (bucket_id = 'avatars'); -- Authenticated write CREATE POLICY "Authenticated upload avatars" ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'avatars'); ``` **Use case**: Public profile pictures, logos, marketing assets. ## Pattern 3: Tenant-Scoped Files Users can only access files from their organization: ```sql CREATE POLICY "Tenant file isolation" ON storage.objects FOR SELECT TO authenticated USING ( bucket_id = 'documents' AND (storage.foldername(name))[1] = ((select auth.jwt()) ->> 'org_id') ); CREATE POLICY "Tenant file uploads" ON storage.objects FOR INSERT TO authenticated WITH CHECK ( bucket_id = 'documents' AND (storage.foldername(name))[1] = ((select auth.jwt()) ->> 'org_id') ); ``` **Folder Structure**: `documents/{org_id}/{file_id}.pdf` ## Pattern 4: Delete Own Files Users can delete their own files: ```sql CREATE POLICY "Users delete own files" ON storage.objects FOR DELETE TO authenticated USING ( bucket_id = 'avatars' AND (select auth.uid())::text = (storage.foldername(name))[1] ); ``` ## Pattern 5: File Overwriting (upsert) For file overwriting via `upsert` option, grant SELECT + UPDATE: ```sql -- Allow reading (required for upsert check) CREATE POLICY "Users read own files" ON storage.objects FOR SELECT TO authenticated USING ( bucket_id = 'avatars' AND (select auth.uid())::text = (storage.foldername(name))[1] ); -- Allow updating (for upsert) CREATE POLICY "Users update own files" ON storage.objects FOR UPDATE TO authenticated USING ( bucket_id = 'avatars' AND (select auth.uid())::text = (storage.foldername(name))[1] ) WITH CHECK ( bucket_id = 'avatars' AND (select auth.uid())::text = (storage.foldername(name))[1] ); ``` ## Bucket Configuration Create buckets with appropriate public/private settings: ```sql -- Create private bucket (requires policies) INSERT INTO storage.buckets (id, name, public) VALUES ('avatars', 'avatars', false); -- Create public bucket (files publicly accessible by URL) INSERT INTO storage.buckets (id, name, public) VALUES ('public-images', 'public-images', true); ``` **Note**: Even public buckets respect RLS for uploads/deletes. ## Helper Functions ```sql -- Extract user folder from path CREATE OR REPLACE FUNCTION storage.user_owns_file(file_path text) RETURNS boolean LANGUAGE sql STABLE AS $$ SELECT (select auth.uid())::text = (storage.foldername(file_path))[1]; $$; -- Usage in policy CREATE POLICY "Users access own files" ON storage.objects FOR ALL TO authenticated USING ( bucket_id = 'private' AND storage.user_owns_file(name) ); ``` ## Security Considerations 1. **Always validate bucket_id** in policies (prevent cross-bucket access) 2. **Use folder structure** for user/tenant isolation 3. **Set appropriate bucket public/private** settings 4. **Monitor storage size** per user/tenant (implement quotas) 5. **Validate file types** server-side (policies can't check content) ## Testing Test storage policies with Supabase client: ```typescript // Test upload as user A const { data, error } = await supabase.storage .from('avatars') .upload(`${user.id}/avatar.jpg`, file); // Should succeed for own folder expect(error).toBeNull(); // Test read as user B (should fail for user A's folder) const { data: files, error: listError } = await supabase.storage .from('avatars') .list(`${otherUserId}/`); // Should return empty or error expect(files).toHaveLength(0); ``` ## Performance Storage policies are evaluated on every file operation. **Optimize**: - Use simple folder path checks (fast) - Avoid complex JOINs in storage policies - Cache JWT claims in helper functions - Use indexes on bucket_id (already indexed by Supabase) ## Common Pitfalls - **Forgetting SELECT policy** for upsert operations - **Not validating bucket_id** (allows cross-bucket access) - **Complex policies** (slow file operations) - **Mixing public/private** bucket settings incorrectly elicit: false - id: performance-optimization title: "RLS Performance Optimization" instruction: | Critical performance optimizations for RLS policies validated by Supabase documentation. ## 🚀 Optimization 1: Wrap Auth Functions with SELECT (94.97% faster) **Critical Discovery** from Supabase docs: Wrapping auth functions enables query caching. ### ❌ SLOW (no caching): ```sql CREATE POLICY "users_select" ON users FOR SELECT TO authenticated USING (auth.uid() = user_id); ``` ### ✅ FAST (cached, 94.97% improvement): ```sql CREATE POLICY "users_select" ON users FOR SELECT TO authenticated USING ((select auth.uid()) = user_id); ``` **Why**: PostgreSQL caches the result of `(select auth.uid())` for the duration of the transaction, avoiding repeated function calls. **Impact**: **19x faster queries** in high-traffic scenarios. **Apply to all auth functions**: - `(select auth.uid())` - `(select auth.jwt())` - `(select auth.email())` ## 🚀 Optimization 2: Index Policy Columns (99.94% improvement) **Always index columns used in policy expressions.** ```sql -- Policy uses user_id CREATE POLICY "user_policy" ON posts USING ((select auth.uid()) = user_id); -- Index user_id (99.94% faster) CREATE INDEX idx_posts_user_id ON posts(user_id); ``` **Critical indexes**: ```sql -- Tenant isolation CREATE INDEX idx_table_org_id ON table_name(org_id); -- Owner-based policies CREATE INDEX idx_table_user_id ON table_name(user_id); -- Time-based policies CREATE INDEX idx_table_scheduling ON table_name(publish_at, expire_at) WHERE publish_at IS NOT NULL OR expire_at IS NOT NULL; ``` ## 🚀 Optimization 3: Filter Client-Side Explicitly **Even with RLS policies, explicitly filter in client queries.** ```typescript // ❌ Relies only on RLS const { data } = await supabase .from('users') .select('*'); // Returns only own data due to RLS, but query planner doesn't know // ✅ Explicit filter (helps query planner) const { data } = await supabase .from('users') .select('*') .eq('user_id', userId); // Query planner can use index ``` **Why**: Explicit filters help PostgreSQL query planner choose optimal index scan. ## 🚀 Optimization 4: Specify Roles Explicitly ```sql -- ❌ Applies to all roles (unnecessary checks) CREATE POLICY "policy" ON table USING (...); -- ✅ Specific role (fewer checks) CREATE POLICY "policy" ON table TO authenticated -- Only authenticated users USING (...); ``` **Common roles**: - `TO authenticated` - Logged-in users - `TO anon` - Anonymous users - `TO public` - Both authenticated and anon ## 🚀 Optimization 5: Use Security Definer Functions **Bypass RLS on join tables** with security definer functions. ```sql -- ❌ Policy with JOIN (slow, 99.99% slower) CREATE POLICY "team_access" ON documents USING ( team_id IN ( SELECT team_id FROM user_teams WHERE user_id = (select auth.uid()) ) ); -- ✅ Security definer function (99.99% improvement) CREATE OR REPLACE FUNCTION user_team_ids() RETURNS TABLE(team_id UUID) LANGUAGE sql STABLE SECURITY DEFINER AS $$ SELECT team_id FROM user_teams WHERE user_id = auth.uid(); $$; CREATE POLICY "team_access" ON documents USING (team_id IN (SELECT user_team_ids())); ``` **Why**: Security definer functions run with elevated privileges, bypassing RLS on join tables. ## 🚀 Optimization 6: Minimize Joins **Avoid joining source and target tables in policies.** ```sql -- ❌ JOIN in policy (slow) CREATE POLICY "org_access" ON documents USING ( EXISTS ( SELECT 1 FROM users WHERE users.id = (select auth.uid()) AND users.org_id = documents.org_id ) ); -- ✅ Use JWT claims (no JOIN) CREATE POLICY "org_access" ON documents USING ( org_id = ((select auth.jwt()) ->> 'org_id')::uuid ); ``` **Strategy**: Store necessary claims in JWT (org_id, role, tenant_id). ## Performance Checklist Apply to every RLS policy: - [ ] Wrap `auth.uid()` with `(select auth.uid())` - [ ] Index all columns used in USING/WITH CHECK - [ ] Specify role explicitly (TO authenticated vs TO public) - [ ] Use JWT claims instead of JOINs where possible - [ ] Create security definer functions for complex permission checks - [ ] Test query plans with EXPLAIN ANALYZE - [ ] Filter client-side explicitly ## Measuring Performance ```sql -- Check query plan (look for Sequential Scan vs Index Scan) EXPLAIN ANALYZE SELECT * FROM users WHERE user_id = 'xxx'; -- Monitor slow queries SELECT query, calls, mean_exec_time, max_exec_time FROM pg_stat_statements WHERE query LIKE '%users%' ORDER BY mean_exec_time DESC LIMIT 10; ``` ## Real-World Impact **Before optimization**: - Query time: 250ms - Database CPU: 80% - Queries/sec: 40 **After optimization** (wrapped functions + indexes): - Query time: 12ms (95% improvement) - Database CPU: 15% - Queries/sec: 800 (20x increase) elicit: false - id: advanced-patterns title: "Advanced RLS Patterns" instruction: | Advanced RLS patterns beyond basic owner-only and tenant isolation. ## Pattern 6: Time-Based Access (Scheduled Content) **Use case**: Blog posts with scheduled publishing, promotions with expiration, time-limited content. ```sql CREATE POLICY "scheduled_content" ON posts FOR SELECT TO authenticated USING ( (publish_at IS NULL OR publish_at <= NOW()) AND (expire_at IS NULL OR expire_at > NOW()) ); ``` **How it works**: - `publish_at IS NULL` - No schedule, always visible - `publish_at <= NOW()` - Past publish date, visible - `expire_at IS NULL` - No expiration, always visible - `expire_at > NOW()` - Not expired, visible **Performance**: ```sql -- Index for time-based queries CREATE INDEX idx_posts_scheduling ON posts(publish_at, expire_at) WHERE publish_at IS NOT NULL OR expire_at IS NOT NULL; ``` **Client-side filtering** (helps query planner): ```typescript const { data } = await supabase .from('posts') .select('*') .lte('publish_at', new Date().toISOString()) .or('publish_at.is.null') .gte('expire_at', new Date().toISOString()) .or('expire_at.is.null'); ``` ## Pattern 7: Hierarchical Organizations (Detailed) **Use case**: Org > Team > User hierarchy with different access levels. ### Simple Hierarchy (Org-Level): ```sql CREATE POLICY "org_hierarchy" ON resources FOR SELECT TO authenticated USING ( org_id IN ( SELECT org_id FROM user_org_memberships WHERE user_id = (select auth.uid()) ) ); ``` **User sees resources from ALL orgs they belong to.** ### Complex Hierarchy (Org + Team): ```sql -- Option 1: User sees resources from their teams CREATE POLICY "team_hierarchy" ON resources FOR SELECT TO authenticated USING ( team_id IN ( SELECT team_id FROM user_team_memberships WHERE user_id = (select auth.uid()) ) ); -- Option 2: Combined (org admin sees all, team member sees team only) CREATE POLICY "combined_hierarchy" ON resources FOR SELECT TO authenticated USING ( -- Org admin sees all resources in org ( org_id IN ( SELECT org_id FROM user_org_memberships WHERE user_id = (select auth.uid()) AND role = 'admin' ) ) OR -- Team member sees only team resources ( team_id IN ( SELECT team_id FROM user_team_memberships WHERE user_id = (select auth.uid()) ) ) ); ``` **Performance**: ```sql -- Indexes for hierarchy lookups CREATE INDEX idx_user_org_memberships_user ON user_org_memberships(user_id, org_id); CREATE INDEX idx_user_team_memberships_user ON user_team_memberships(user_id, team_id); CREATE INDEX idx_resources_org_id ON resources(org_id); CREATE INDEX idx_resources_team_id ON resources(team_id); ``` **Optimization** (security definer function): ```sql CREATE OR REPLACE FUNCTION user_accessible_orgs() RETURNS TABLE(org_id UUID) LANGUAGE sql STABLE SECURITY DEFINER AS $$ SELECT org_id FROM user_org_memberships WHERE user_id = auth.uid(); $$; CREATE POLICY "org_hierarchy_optimized" ON resources FOR SELECT TO authenticated USING (org_id IN (SELECT user_accessible_orgs())); ``` ## Pattern 8: Role-Based with Custom Claims (Advanced) **Use case**: Different permissions per role (admin, manager, analyst, user). ### Setup: Add role to JWT ```sql CREATE OR REPLACE FUNCTION custom_access_token_hook(event jsonb) RETURNS jsonb AS $$ DECLARE claims jsonb; user_role text; user_org_id uuid; BEGIN -- Get user role and org from profiles table SELECT role, org_id INTO user_role, user_org_id FROM public.user_profiles WHERE user_id = (event->>'user_id')::uuid; -- Add to JWT claims claims := event->'claims'; claims := jsonb_set(claims, '{role}', to_jsonb(user_role)); claims := jsonb_set(claims, '{org_id}', to_jsonb(user_org_id)); RETURN jsonb_set(event, '{claims}', claims); END; $$ LANGUAGE plpgsql SECURITY DEFINER; ``` **Configure in Supabase Dashboard**: Authentication > Hooks > Custom Access Token ### Policy: Role-Based Access ```sql -- Admin sees all CREATE POLICY "admin_full_access" ON sensitive_data FOR ALL TO authenticated USING ( ((select auth.jwt()) ->> 'role') = 'admin' ); -- Manager sees org data CREATE POLICY "manager_org_access" ON sensitive_data FOR SELECT TO authenticated USING ( ((select auth.jwt()) ->> 'role') = 'manager' AND org_id = ((select auth.jwt()) ->> 'org_id')::uuid ); -- User sees own data only CREATE POLICY "user_own_access" ON sensitive_data FOR SELECT TO authenticated USING ( ((select auth.jwt()) ->> 'role') = 'user' AND user_id = (select auth.uid()) ); ``` ### Policy: Role Hierarchy (Admin > Manager > User) ```sql CREATE POLICY "role_hierarchy" ON resources FOR ALL TO authenticated USING ( CASE ((select auth.jwt()) ->> 'role') WHEN 'admin' THEN true -- Admin sees everything WHEN 'manager' THEN org_id = ((select auth.jwt()) ->> 'org_id')::uuid ELSE user_id = (select auth.uid()) -- User sees own only END ); ``` ## Pattern 9: Multi-Factor Authentication (AAL2) **Use case**: Sensitive operations require MFA. ```sql CREATE POLICY "mfa_required_for_sensitive_ops" ON sensitive_operations FOR INSERT TO authenticated USING ( ((select auth.jwt()) ->> 'aal') = 'aal2' -- Assurance Level 2 (MFA) ); ``` **AAL levels**: - `aal1` - Single factor (password only) - `aal2` - Multi-factor (password + OTP/biometric) ## Pattern 10: IP-Based Restrictions **Use case**: Restrict admin operations to office IP. ```sql CREATE POLICY "admin_office_only" ON admin_operations FOR ALL TO authenticated USING ( ((select auth.jwt()) ->> 'role') = 'admin' AND inet_client_addr() << '192.168.1.0/24'::inet -- Office network ); ``` **Note**: `inet_client_addr()` returns client IP. ## Advanced Patterns Summary | Pattern | Use Case | Complexity | Performance Impact | |---------|----------|------------|-------------------| | Time-based | Scheduled content | Low | Low (with index) | | Hierarchical | Org > Team > User | Medium | Medium (needs indexes) | | Role-based claims | RBAC | Low | Low (JWT cached) | | AAL2 MFA | Sensitive ops | Low | None | | IP restrictions | Office-only | Low | None | **Best practices**: - Prefer JWT claims over database lookups (faster) - Always index columns used in policies - Use security definer functions for complex checks - Test with EXPLAIN ANALYZE - Wrap auth functions with SELECT for caching elicit: false - id: testing title: "RLS Testing Strategy" instruction: | How to test and validate RLS policies: ## Unit Tests Test individual policies with different auth contexts: ```sql -- Test as user A SET request.jwt.claims = '{"sub": "user-a-uuid"}'; SELECT * FROM table_name; -- Should only see user A's records -- Test as user B SET request.jwt.claims = '{"sub": "user-b-uuid"}'; SELECT * FROM table_name; -- Should only see user B's records ``` ## Integration Tests Test with actual Supabase client: ```typescript // Test authenticated access const { data, error } = await supabase .from('table_name') .select('*'); // Verify only authorized records returned ``` ## Security Audit Checklist for RLS validation: - [ ] All tables with sensitive data have RLS enabled - [ ] No accidental policy holes (test with unauthorized users) - [ ] Service role usage is documented and justified - [ ] Policies perform well (no slow queries) - [ ] Cross-tenant data leakage tested - [ ] Anonymous vs authenticated access verified - [ ] Edge cases tested (null values, missing context) ## Automated Testing Script or framework for continuous validation. elicit: false - id: migration title: "RLS Migration Scripts" instruction: | Concrete SQL migration for implementing these policies: ## Migration: `YYYYMMDDHHMMSS_add_rls_policies.sql` ```sql -- Enable RLS on tables ALTER TABLE table1 ENABLE ROW LEVEL SECURITY; ALTER TABLE table2 ENABLE ROW LEVEL SECURITY; -- Drop existing policies if re-running DROP POLICY IF EXISTS "policy_name" ON table_name; -- Create policies CREATE POLICY "policy_name_select" ON table_name FOR SELECT TO authenticated USING (auth.uid() = user_id); CREATE POLICY "policy_name_insert" ON table_name FOR INSERT TO authenticated WITH CHECK (auth.uid() = user_id); -- ... additional policies ... -- Create indexes to support policies CREATE INDEX IF NOT EXISTS idx_table_user_id ON table_name(user_id); -- Grant permissions GRANT SELECT, INSERT, UPDATE, DELETE ON table_name TO authenticated; GRANT SELECT ON table_name TO anon; ``` ## Rollback Migration ```sql -- Remove policies DROP POLICY IF EXISTS "policy_name_select" ON table_name; DROP POLICY IF EXISTS "policy_name_insert" ON table_name; -- Disable RLS ALTER TABLE table_name DISABLE ROW LEVEL SECURITY; ``` elicit: false - id: monitoring title: "RLS Monitoring & Debugging" instruction: | How to monitor and debug RLS policies: ## Query Performance Identify slow queries caused by RLS: ```sql -- Check query plans EXPLAIN ANALYZE SELECT * FROM table_name; ``` ## Policy Effectiveness Verify policies are being applied: ```sql -- Check active policies SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual FROM pg_policies WHERE tablename = 'your_table'; ``` ## Common Issues ### Issue: Policy not applying - Check RLS is enabled on table - Verify user role matches policy target - Check auth context is set correctly ### Issue: Performance degradation - Add indexes for policy columns - Simplify policy expressions - Consider denormalization ### Issue: Unexpected access - Audit all policies on table - Check for permissive vs restrictive policies - Verify no service role leakage ## Logging Log policy violations or unexpected access patterns. elicit: false - id: best-practices title: "RLS Best Practices" instruction: | ## Design Principles 1. **Start Restrictive**: Default deny, explicitly allow 2. **Minimize Policy Complexity**: Simpler policies are easier to audit 3. **Use Helper Functions**: Encapsulate complex logic 4. **Index Policy Columns**: Performance is critical 5. **Test Extensively**: Security bugs are costly 6. **Document Everything**: Future you will thank you 7. **Audit Regularly**: Policies drift over time 8. **Avoid Service Role**: Use it only when absolutely necessary ## Common Pitfalls - Forgetting to enable RLS on new tables - Policy expressions with poor performance - Not testing with actual user contexts - Overly permissive catch-all policies - Mixing USING and WITH CHECK incorrectly - Not considering null values in policies ## Security Checklist - [ ] All sensitive tables have RLS enabled - [ ] Policies tested with unauthorized users - [ ] Anonymous access limited to public data only - [ ] Service role usage is minimal and documented - [ ] Policies use indexes for performance - [ ] Multi-tenant isolation is verified - [ ] Edge cases tested (nulls, empty results) - [ ] Policies are documented and reviewed elicit: false