From 9cf462a7c1be31764475bc79da094ca09795744b Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sat, 29 Nov 2025 18:46:56 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 12 + README.md | 3 + plugin.lock.json | 61 +++ skills/supabase-rls-policy-generator/SKILL.md | 246 +++++++++++ .../assets/policy-documentation-template.md | 252 +++++++++++ .../assets/policy-templates.sql | 396 ++++++++++++++++++ .../references/rls-patterns.md | 393 +++++++++++++++++ .../references/supabase-auth.md | 303 ++++++++++++++ 8 files changed, 1666 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 plugin.lock.json create mode 100644 skills/supabase-rls-policy-generator/SKILL.md create mode 100644 skills/supabase-rls-policy-generator/assets/policy-documentation-template.md create mode 100644 skills/supabase-rls-policy-generator/assets/policy-templates.sql create mode 100644 skills/supabase-rls-policy-generator/references/rls-patterns.md create mode 100644 skills/supabase-rls-policy-generator/references/supabase-auth.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..d688e11 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "supabase-rls-policy-generator", + "description": "This skill should be used when the user requests to generate, create, or add Row-Level Security (RLS) policies for Supabase databases in multi-tenant or role-based applications. It generates comprehensive RLS policies using auth.uid(), auth.jwt() claims, and role-based access patterns. Trigger terms include RLS, row level security, supabase security, generate policies, auth policies, multi-tenant security, role-based access, database security policies, supabase permissions, tenant isolation.", + "version": "1.0.0", + "author": { + "name": "Hope Overture", + "email": "support@worldbuilding-app-skills.dev" + }, + "skills": [ + "./skills" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fcc3f31 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# supabase-rls-policy-generator + +This skill should be used when the user requests to generate, create, or add Row-Level Security (RLS) policies for Supabase databases in multi-tenant or role-based applications. It generates comprehensive RLS policies using auth.uid(), auth.jwt() claims, and role-based access patterns. Trigger terms include RLS, row level security, supabase security, generate policies, auth policies, multi-tenant security, role-based access, database security policies, supabase permissions, tenant isolation. diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..d635bfb --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,61 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:hopeoverture/worldbuilding-app-skills:plugins/supabase-rls-policy-generator", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "9dc70ff85f9cc02392469b553fc7f43956562474", + "treeHash": "c9851f2b5d70ed6a0e59bfa9ba5703a9ee0e747f5fc4cd0d3da28ee11578491b", + "generatedAt": "2025-11-28T10:17:30.411244Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "supabase-rls-policy-generator", + "description": "This skill should be used when the user requests to generate, create, or add Row-Level Security (RLS) policies for Supabase databases in multi-tenant or role-based applications. It generates comprehensive RLS policies using auth.uid(), auth.jwt() claims, and role-based access patterns. Trigger terms include RLS, row level security, supabase security, generate policies, auth policies, multi-tenant security, role-based access, database security policies, supabase permissions, tenant isolation.", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "d2f2b90edb4f7e9963ab139bb5d8c8010e03fb0fa3603de6d036192704663462" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "8a9d5f4bb8a2752dc20d7128d0aadd29a9044e7dcecc0c9ee6d60d0348ce96e5" + }, + { + "path": "skills/supabase-rls-policy-generator/SKILL.md", + "sha256": "f404dd551c98507aeab7bde8453cb4c10cf25268b6d6f3470379b41741ad4859" + }, + { + "path": "skills/supabase-rls-policy-generator/references/supabase-auth.md", + "sha256": "ed0080224fe24da2a27cf79d8e0237aa28e9787569c2dede65a2bde58046b4c8" + }, + { + "path": "skills/supabase-rls-policy-generator/references/rls-patterns.md", + "sha256": "8b9db4aaec6c21d9c3edf72d9d029d5d0ec597d0c890ee420ffee2cf04ed7c23" + }, + { + "path": "skills/supabase-rls-policy-generator/assets/policy-templates.sql", + "sha256": "6a2f3a70dcbaddba304c9c339e7d68541460ab664be0e258778b297051cbd64c" + }, + { + "path": "skills/supabase-rls-policy-generator/assets/policy-documentation-template.md", + "sha256": "d5df28d015c20d3a4fc96e0fd6f3a3be55439dc094bb419540b65850bd4f046a" + } + ], + "dirSha256": "c9851f2b5d70ed6a0e59bfa9ba5703a9ee0e747f5fc4cd0d3da28ee11578491b" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/supabase-rls-policy-generator/SKILL.md b/skills/supabase-rls-policy-generator/SKILL.md new file mode 100644 index 0000000..fb63944 --- /dev/null +++ b/skills/supabase-rls-policy-generator/SKILL.md @@ -0,0 +1,246 @@ +--- +name: supabase-rls-policy-generator +description: This skill should be used when the user requests to generate, create, or add Row-Level Security (RLS) policies for Supabase databases in multi-tenant or role-based applications. It generates comprehensive RLS policies using auth.uid(), auth.jwt() claims, and role-based access patterns. Trigger terms include RLS, row level security, supabase security, generate policies, auth policies, multi-tenant security, role-based access, database security policies, supabase permissions, tenant isolation. +--- + +# Supabase RLS Policy Generator + +To generate comprehensive Row-Level Security policies for Supabase databases, follow these steps systematically. + +## Step 1: Analyze Current Schema + +Before generating policies: +1. Ask user for the database schema file path or table names +2. Read the schema to understand table structures, foreign keys, and relationships +3. Identify tables that need RLS protection +4. Determine the security model: multi-tenant, role-based, or hybrid + +## Step 2: Identify Security Requirements + +Determine access patterns by asking: +- Is this a multi-tenant application? (tenant_id isolation) +- What roles exist in the system? (admin, user, viewer, etc.) +- Are there public vs private resources? +- Do users need to share resources across accounts? +- Are there hierarchical permissions? (organization > team > user) + +Consult `references/rls-patterns.md` for common security patterns. + +## Step 3: Generate RLS Policies + +For each table requiring protection, generate policies following this structure: + +### Enable RLS +```sql +ALTER TABLE table_name ENABLE ROW LEVEL SECURITY; +``` + +### Policy Types to Generate + +**SELECT Policies** - Control read access: +- User can view their own records +- User can view records in their tenant +- Role-based viewing (admins see all) +- Public records accessible to all authenticated users + +**INSERT Policies** - Control creation: +- User can create records with their own user_id +- User can create records in their tenant +- Role-based creation restrictions + +**UPDATE Policies** - Control modifications: +- User can update their own records +- Admins can update all records +- Tenant-scoped updates + +**DELETE Policies** - Control deletion: +- User can delete their own records +- Admin-only deletion +- Tenant-scoped deletion + +### Policy Templates + +Use templates from `assets/policy-templates.sql`: + +**Basic User Ownership**: +```sql +CREATE POLICY "Users can view own records" + ON table_name FOR SELECT + USING (auth.uid() = user_id); +``` + +**Multi-Tenant Isolation**: +```sql +CREATE POLICY "Tenant isolation" + ON table_name FOR ALL + USING ( + tenant_id IN ( + SELECT tenant_id FROM user_tenants + WHERE user_id = auth.uid() + ) + ); +``` + +**Role-Based Access**: +```sql +CREATE POLICY "Admins have full access" + ON table_name FOR ALL + USING ( + auth.jwt() ->> 'role' = 'admin' + ); +``` + +**JWT Claims**: +```sql +CREATE POLICY "Organization access" + ON table_name FOR SELECT + USING ( + organization_id = (auth.jwt() -> 'app_metadata' ->> 'organization_id')::uuid + ); +``` + +## Step 4: Generate Helper Functions + +Create PostgreSQL functions to support complex policies: + +```sql +-- Function to check user role +CREATE OR REPLACE FUNCTION auth.user_has_role(required_role TEXT) +RETURNS BOOLEAN AS $$ +BEGIN + RETURN (auth.jwt() ->> 'role') = required_role; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Function to check tenant membership +CREATE OR REPLACE FUNCTION auth.user_in_tenant(target_tenant_id UUID) +RETURNS BOOLEAN AS $$ +BEGIN + RETURN EXISTS ( + SELECT 1 FROM user_tenants + WHERE user_id = auth.uid() + AND tenant_id = target_tenant_id + ); +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; +``` + +## Step 5: Generate Testing Queries + +Create test queries to verify policies work correctly: + +```sql +-- Test as authenticated user +SET request.jwt.claim.sub = 'user-uuid'; +SELECT * FROM table_name; -- Should see only accessible records + +-- Test as admin +SET request.jwt.claim.role = 'admin'; +SELECT * FROM table_name; -- Should see all records + +-- Test as different tenant +SET request.jwt.claim.sub = 'other-user-uuid'; +SELECT * FROM table_name; -- Should see different tenant's records +``` + +## Step 6: Create Migration File + +Generate a migration file with proper structure: + +```sql +-- Migration: Add RLS policies +-- Created: [timestamp] + +-- Enable RLS on tables +ALTER TABLE users ENABLE ROW LEVEL SECURITY; +ALTER TABLE items ENABLE ROW LEVEL SECURITY; + +-- Drop existing policies if any +DROP POLICY IF EXISTS "policy_name" ON table_name; + +-- Create new policies +[Generated policies here] + +-- Create helper functions +[Generated functions here] + +-- Grant necessary permissions +GRANT SELECT, INSERT, UPDATE, DELETE ON table_name TO authenticated; +GRANT SELECT ON table_name TO anon; -- If public read needed +``` + +## Step 7: Document Generated Policies + +Create documentation explaining: +- What each policy does +- Which users/roles have what access +- Any special cases or exceptions +- How to test the policies +- Common troubleshooting tips + +Use template from `assets/policy-documentation-template.md`. + +## Implementation Guidelines + +### Security Best Practices +- Always enable RLS on tables with user data +- Use auth.uid() for user-owned records +- Use JWT claims for role-based access +- Prefer SECURITY DEFINER functions for complex logic +- Test policies with different user roles +- Use USING clause for read access, WITH CHECK for write validation + +### Performance Considerations +- Add indexes on columns used in policies (user_id, tenant_id, role) +- Keep policy logic simple for better performance +- Use helper functions for reusable complex logic +- Avoid subqueries in policies when possible + +### Common Patterns + +Consult `references/rls-patterns.md` for detailed examples of: +- Multi-tenant isolation +- Role-based access control (RBAC) +- Attribute-based access control (ABAC) +- Hierarchical permissions +- Public/private resource splitting +- Shared resource access + +## Output Format + +Generate files in the following structure: +``` +migrations/ + [timestamp]_add_rls_policies.sql +docs/ + rls-policies.md (documentation) +tests/ + rls_tests.sql (test queries) +``` + +## Verification Checklist + +Before completing: +- [ ] RLS enabled on all sensitive tables +- [ ] Policies cover all operations (SELECT, INSERT, UPDATE, DELETE) +- [ ] Policies tested with different user roles +- [ ] Indexes added for policy columns +- [ ] Helper functions created for complex logic +- [ ] Documentation generated +- [ ] Test queries provided +- [ ] No policies accidentally grant excessive access + +## Consulting References + +Throughout generation: +- Consult `references/rls-patterns.md` for security patterns +- Consult `references/supabase-auth.md` for auth.uid() and JWT structure +- Use templates from `assets/policy-templates.sql` + +## Completion + +When finished: +1. Display the generated migration file +2. Summarize the policies created +3. Provide testing instructions +4. Offer to generate additional policies or modify existing ones diff --git a/skills/supabase-rls-policy-generator/assets/policy-documentation-template.md b/skills/supabase-rls-policy-generator/assets/policy-documentation-template.md new file mode 100644 index 0000000..e1338fb --- /dev/null +++ b/skills/supabase-rls-policy-generator/assets/policy-documentation-template.md @@ -0,0 +1,252 @@ +# RLS Policies Documentation: [Table Name] + +**Generated**: [Date] +**Database**: [Database Name] +**Schema**: public + +## Overview + +This document describes the Row-Level Security (RLS) policies implemented for the `[table_name]` table. + +**Security Model**: [Multi-tenant / Role-based / Hybrid / etc.] + +## Table Schema + +```sql +CREATE TABLE [table_name] ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES auth.users(id), + tenant_id UUID REFERENCES tenants(id), + [other columns...] + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); +``` + +**Security-relevant columns**: +- `user_id`: Owner of the record +- `tenant_id`: Tenant/organization identifier +- [other relevant columns...] + +## RLS Status + +```sql +-- RLS is ENABLED on this table +ALTER TABLE [table_name] ENABLE ROW LEVEL SECURITY; +``` + +## Policies + +### Policy 1: [Policy Name] + +**Operation**: SELECT | INSERT | UPDATE | DELETE | ALL +**Policy Name**: `[policy_name]` + +**Purpose**: [What this policy does] + +**SQL**: +```sql +CREATE POLICY "[policy_name]" + ON [table_name] FOR [OPERATION] + USING ([condition]) + WITH CHECK ([condition]); +``` + +**Access Rules**: +- [Who] can [do what] when [condition] +- Example: "Users can view records where they are the owner" +- Example: "Admins can view all records" + +**Testing**: +```sql +-- Test as user [user_id] +SET request.jwt.claim.sub = '[user-uuid]'; +SELECT * FROM [table_name]; -- Expected: [description] + +-- Test as admin +SET request.jwt.claim.role = 'admin'; +SELECT * FROM [table_name]; -- Expected: [description] +``` + +--- + +### Policy 2: [Policy Name] + +[Repeat structure for each policy...] + +## Access Matrix + +| Role / Context | SELECT | INSERT | UPDATE | DELETE | +|---------------|--------|--------|--------|--------| +| Anonymous | No | No | No | No | +| Authenticated User | Own records | Own records | Own records | Own records | +| Team Member | Team records | Team records | Team records | No | +| Admin | All records | All records | All records | All records | + +## Helper Functions + +### Function: [function_name] + +**Purpose**: [What this function does] + +**SQL**: +```sql +CREATE OR REPLACE FUNCTION [function_name]([params]) +RETURNS [type] AS $$ +BEGIN + [implementation] +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; +``` + +**Usage in Policies**: +```sql +USING ([function_name](param)) +``` + +## Indexes + +The following indexes support RLS policy performance: + +```sql +CREATE INDEX idx_[table]_user_id ON [table_name](user_id); +CREATE INDEX idx_[table]_tenant_id ON [table_name](tenant_id); +-- [other indexes...] +``` + +## Permissions + +```sql +-- Authenticated users can perform CRUD operations (filtered by RLS) +GRANT SELECT, INSERT, UPDATE, DELETE ON [table_name] TO authenticated; + +-- Anonymous users can read public records (filtered by RLS) +GRANT SELECT ON [table_name] TO anon; + +-- Sequence usage +GRANT USAGE, SELECT ON SEQUENCE [table_name]_id_seq TO authenticated; +``` + +## Testing Scenarios + +### Scenario 1: User Access Own Records + +**Setup**: +```sql +-- Create test user +INSERT INTO auth.users (id, email) VALUES + ('user-1-uuid', 'user1@example.com'); + +-- Create test records +INSERT INTO [table_name] (user_id, [data]) +VALUES ('user-1-uuid', 'data1'); +``` + +**Test**: +```sql +-- Set auth context +SET request.jwt.claim.sub = 'user-1-uuid'; + +-- User should see own record +SELECT * FROM [table_name]; +-- Expected: 1 row + +-- User should not see other users' records +SELECT * FROM [table_name] WHERE user_id != 'user-1-uuid'; +-- Expected: 0 rows +``` + +### Scenario 2: [Other scenarios...] + +[Add more test scenarios as needed] + +## Security Considerations + +### Enforced Restrictions +- [x] Users cannot access other users' private data +- [x] Users cannot modify tenant_id after creation +- [x] Anonymous users have read-only access to public data +- [x] [Other restrictions...] + +### Known Limitations +- [Any limitations or edge cases] +- [Performance considerations] +- [Future improvements needed] + +## Common Issues and Troubleshooting + +### Issue 1: "Permission denied for relation [table_name]" + +**Cause**: RLS is enabled but no policies match the user's context. + +**Solution**: +1. Verify user is authenticated: `SELECT auth.uid();` +2. Check user's role: `SELECT auth.jwt() ->> 'role';` +3. Verify policy conditions match user context + +### Issue 2: Policies not applying correctly + +**Cause**: Testing as superuser, which bypasses RLS. + +**Solution**: +```sql +-- Test as non-superuser +SET ROLE non_superuser_role; +-- Or use Supabase client with actual auth token +``` + +### Issue 3: Performance degradation + +**Cause**: Missing indexes on columns used in policies. + +**Solution**: +```sql +-- Add indexes +CREATE INDEX idx_[table]_[column] ON [table_name]([column]); +-- Analyze table +ANALYZE [table_name]; +``` + +## Migration History + +### Initial RLS Setup +- **Date**: [Date] +- **Migration**: `[timestamp]_add_rls_policies.sql` +- **Changes**: Enabled RLS and created initial policies + +### [Future updates...] + +## Maintenance + +### Regular Tasks +- [ ] Review policy effectiveness quarterly +- [ ] Monitor query performance +- [ ] Audit access logs for anomalies +- [ ] Update policies when roles/permissions change + +### Performance Monitoring +```sql +-- Check policy execution time +EXPLAIN ANALYZE +SELECT * FROM [table_name] +WHERE [typical_condition]; + +-- Check index usage +SELECT schemaname, tablename, indexname, idx_scan +FROM pg_stat_user_indexes +WHERE tablename = '[table_name]'; +``` + +## References + +- Supabase RLS Documentation: https://supabase.com/docs/guides/auth/row-level-security +- PostgreSQL RLS Policies: https://www.postgresql.org/docs/current/ddl-rowsecurity.html +- Auth Functions: https://supabase.com/docs/guides/database/postgres/row-level-security + +## Change Log + +| Date | Author | Change | +|------|--------|--------| +| [Date] | [Name] | Initial policies created | +| [Date] | [Name] | Added admin access policy | +| ... | ... | ... | diff --git a/skills/supabase-rls-policy-generator/assets/policy-templates.sql b/skills/supabase-rls-policy-generator/assets/policy-templates.sql new file mode 100644 index 0000000..1d0b1ca --- /dev/null +++ b/skills/supabase-rls-policy-generator/assets/policy-templates.sql @@ -0,0 +1,396 @@ +-- Supabase RLS Policy Templates +-- Copy and adapt these templates for your tables + +-- ============================================================================ +-- TEMPLATE 1: User Ownership (Basic) +-- ============================================================================ +-- Use when: Users can only access their own records +-- Tables: user_profiles, user_settings, user_preferences + +ALTER TABLE table_name ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "users_select_own" + ON table_name FOR SELECT + USING (auth.uid() = user_id); + +CREATE POLICY "users_insert_own" + ON table_name FOR INSERT + WITH CHECK (auth.uid() = user_id); + +CREATE POLICY "users_update_own" + ON table_name FOR UPDATE + USING (auth.uid() = user_id) + WITH CHECK (auth.uid() = user_id); + +CREATE POLICY "users_delete_own" + ON table_name FOR DELETE + USING (auth.uid() = user_id); + +-- ============================================================================ +-- TEMPLATE 2: Multi-Tenant Isolation +-- ============================================================================ +-- Use when: Multiple tenants/organizations sharing the database +-- Tables: documents, projects, team_data + +ALTER TABLE table_name ENABLE ROW LEVEL SECURITY; + +-- Assumes a user_tenants junction table +CREATE POLICY "tenant_isolation" + ON table_name FOR ALL + USING ( + tenant_id IN ( + SELECT tenant_id FROM user_tenants + WHERE user_id = auth.uid() + ) + ); + +-- Alternative: Using JWT claim for tenant ID +CREATE POLICY "tenant_isolation_jwt" + ON table_name FOR ALL + USING ( + tenant_id = (auth.jwt() -> 'app_metadata' ->> 'tenant_id')::uuid + ); + +-- ============================================================================ +-- TEMPLATE 3: Role-Based Access Control (Simple) +-- ============================================================================ +-- Use when: Different access based on user roles +-- Tables: admin_settings, moderation_queue + +ALTER TABLE table_name ENABLE ROW LEVEL SECURITY; + +-- Admins can do everything +CREATE POLICY "admins_all_access" + ON table_name FOR ALL + USING (auth.jwt() ->> 'role' = 'admin'); + +-- Regular users can only read +CREATE POLICY "users_read_only" + ON table_name FOR SELECT + USING (auth.jwt() ->> 'role' = 'authenticated'); + +-- ============================================================================ +-- TEMPLATE 4: Role-Based with Ownership Fallback +-- ============================================================================ +-- Use when: Users can access own records, admins can access all +-- Tables: posts, comments, submissions + +ALTER TABLE table_name ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "select_own_or_admin" + ON table_name FOR SELECT + USING ( + auth.uid() = user_id OR + auth.jwt() ->> 'role' = 'admin' + ); + +CREATE POLICY "insert_own" + ON table_name FOR INSERT + WITH CHECK (auth.uid() = user_id); + +CREATE POLICY "update_own_or_admin" + ON table_name FOR UPDATE + USING ( + auth.uid() = user_id OR + auth.jwt() ->> 'role' = 'admin' + ); + +CREATE POLICY "delete_own_or_admin" + ON table_name FOR DELETE + USING ( + auth.uid() = user_id OR + auth.jwt() ->> 'role' = 'admin' + ); + +-- ============================================================================ +-- TEMPLATE 5: Public/Private Resources +-- ============================================================================ +-- Use when: Some records are public, others are private +-- Tables: articles, galleries, portfolios + +ALTER TABLE table_name ENABLE ROW LEVEL SECURITY; + +-- Everyone (including anon) can view public records +CREATE POLICY "public_records_viewable" + ON table_name FOR SELECT + USING (is_public = true); + +-- Authenticated users can view own private records +CREATE POLICY "private_records_owner" + ON table_name FOR SELECT + USING ( + is_public = false AND + auth.uid() = user_id + ); + +-- Only owner can modify +CREATE POLICY "owner_can_modify" + ON table_name FOR UPDATE + USING (auth.uid() = user_id); + +CREATE POLICY "owner_can_delete" + ON table_name FOR DELETE + USING (auth.uid() = user_id); + +-- ============================================================================ +-- TEMPLATE 6: Hierarchical Permissions (Organization/Team) +-- ============================================================================ +-- Use when: Organization > Team > User hierarchy +-- Tables: projects, resources, team_documents + +ALTER TABLE table_name ENABLE ROW LEVEL SECURITY; + +-- Organization admins see all +CREATE POLICY "org_admin_all" + ON table_name FOR ALL + USING ( + organization_id IN ( + SELECT organization_id FROM user_organizations + WHERE user_id = auth.uid() AND role = 'admin' + ) + ); + +-- Team members see team records +CREATE POLICY "team_member_access" + ON table_name FOR SELECT + USING ( + team_id IN ( + SELECT team_id FROM user_teams + WHERE user_id = auth.uid() + ) + ); + +-- Owner always has access +CREATE POLICY "owner_access" + ON table_name FOR ALL + USING (auth.uid() = owner_id); + +-- ============================================================================ +-- TEMPLATE 7: Shared Resources +-- ============================================================================ +-- Use when: Resources can be shared with specific users +-- Tables: files, documents, notes + +ALTER TABLE table_name ENABLE ROW LEVEL SECURITY; + +-- Owner can do everything +CREATE POLICY "owner_full_access" + ON table_name FOR ALL + USING (auth.uid() = owner_id); + +-- Shared users can read (and maybe write) +CREATE POLICY "shared_users_read" + ON table_name FOR SELECT + USING ( + EXISTS ( + SELECT 1 FROM table_name_shares + WHERE resource_id = table_name.id + AND user_id = auth.uid() + AND (expires_at IS NULL OR expires_at > NOW()) + ) + ); + +-- Shared users with write permission can update +CREATE POLICY "shared_users_write" + ON table_name FOR UPDATE + USING ( + EXISTS ( + SELECT 1 FROM table_name_shares + WHERE resource_id = table_name.id + AND user_id = auth.uid() + AND can_write = true + AND (expires_at IS NULL OR expires_at > NOW()) + ) + ); + +-- ============================================================================ +-- TEMPLATE 8: Time-Based Access +-- ============================================================================ +-- Use when: Content has publish/expire dates +-- Tables: scheduled_posts, campaigns, events + +ALTER TABLE table_name ENABLE ROW LEVEL SECURITY; + +-- Published content is viewable +CREATE POLICY "view_published" + ON table_name FOR SELECT + USING ( + status = 'published' AND + (published_at IS NULL OR published_at <= NOW()) AND + (expires_at IS NULL OR expires_at > NOW()) + ); + +-- Authors can view all states +CREATE POLICY "author_view_all" + ON table_name FOR SELECT + USING (auth.uid() = author_id); + +-- Authors can update before published +CREATE POLICY "author_update_unpublished" + ON table_name FOR UPDATE + USING ( + auth.uid() = author_id AND + (status = 'draft' OR published_at > NOW()) + ); + +-- ============================================================================ +-- TEMPLATE 9: Cascading Permissions (Parent/Child) +-- ============================================================================ +-- Use when: Child records inherit permissions from parent +-- Tables: comments (child of posts), task_items (child of tasks) + +-- Parent table +ALTER TABLE parent_table ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "parent_access" + ON parent_table FOR SELECT + USING ( + auth.uid() = owner_id OR + is_public = true + ); + +-- Child table inherits from parent +ALTER TABLE child_table ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "child_inherit_from_parent" + ON child_table FOR SELECT + USING ( + parent_id IN ( + SELECT id FROM parent_table + WHERE auth.uid() = owner_id OR is_public = true + ) + ); + +-- ============================================================================ +-- TEMPLATE 10: Moderation/Approval Workflow +-- ============================================================================ +-- Use when: Content needs approval before being visible +-- Tables: user_submissions, comments, reviews + +ALTER TABLE table_name ENABLE ROW LEVEL SECURITY; + +-- Users can view approved content +CREATE POLICY "view_approved" + ON table_name FOR SELECT + USING (status = 'approved'); + +-- Users can view own content in any state +CREATE POLICY "view_own" + ON table_name FOR SELECT + USING (auth.uid() = author_id); + +-- Users can create content (starts as pending) +CREATE POLICY "create_as_pending" + ON table_name FOR INSERT + WITH CHECK ( + auth.uid() = author_id AND + status = 'pending' + ); + +-- Users can edit own pending content +CREATE POLICY "update_own_pending" + ON table_name FOR UPDATE + USING ( + auth.uid() = author_id AND + status = 'pending' + ); + +-- Moderators can update status +CREATE POLICY "moderators_approve" + ON table_name FOR UPDATE + USING (auth.jwt() ->> 'role' IN ('moderator', 'admin')) + WITH CHECK (status IN ('approved', 'rejected')); + +-- ============================================================================ +-- HELPER FUNCTIONS +-- ============================================================================ + +-- Check if user has specific role +CREATE OR REPLACE FUNCTION auth.user_has_role(required_role TEXT) +RETURNS BOOLEAN AS $$ +BEGIN + RETURN (auth.jwt() ->> 'role') = required_role; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Check if user is in tenant +CREATE OR REPLACE FUNCTION auth.user_in_tenant(target_tenant_id UUID) +RETURNS BOOLEAN AS $$ +BEGIN + RETURN EXISTS ( + SELECT 1 FROM user_tenants + WHERE user_id = auth.uid() AND tenant_id = target_tenant_id + ); +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Check team membership +CREATE OR REPLACE FUNCTION auth.user_in_team(target_team_id UUID) +RETURNS BOOLEAN AS $$ +BEGIN + RETURN EXISTS ( + SELECT 1 FROM user_teams + WHERE user_id = auth.uid() AND team_id = target_team_id + ); +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Check organization role +CREATE OR REPLACE FUNCTION auth.user_org_role(org_id UUID) +RETURNS TEXT AS $$ +DECLARE + user_role TEXT; +BEGIN + SELECT role INTO user_role + FROM user_organizations + WHERE user_id = auth.uid() AND organization_id = org_id; + + RETURN user_role; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Check if user can access resource +CREATE OR REPLACE FUNCTION auth.can_access_resource( + resource_owner_id UUID, + resource_is_public BOOLEAN DEFAULT false +) +RETURNS BOOLEAN AS $$ +BEGIN + RETURN ( + auth.uid() = resource_owner_id OR + resource_is_public = true OR + auth.jwt() ->> 'role' = 'admin' + ); +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- ============================================================================ +-- INDEXES FOR PERFORMANCE +-- ============================================================================ + +-- Always index columns used in RLS policies +CREATE INDEX IF NOT EXISTS idx_table_user_id ON table_name(user_id); +CREATE INDEX IF NOT EXISTS idx_table_tenant_id ON table_name(tenant_id); +CREATE INDEX IF NOT EXISTS idx_table_owner_id ON table_name(owner_id); +CREATE INDEX IF NOT EXISTS idx_table_is_public ON table_name(is_public); +CREATE INDEX IF NOT EXISTS idx_table_status ON table_name(status); +CREATE INDEX IF NOT EXISTS idx_table_published_at ON table_name(published_at); + +-- Composite indexes for common queries +CREATE INDEX IF NOT EXISTS idx_table_user_status + ON table_name(user_id, status); +CREATE INDEX IF NOT EXISTS idx_table_tenant_created + ON table_name(tenant_id, created_at DESC); + +-- ============================================================================ +-- GRANT PERMISSIONS +-- ============================================================================ + +-- Grant table access to authenticated users +GRANT SELECT, INSERT, UPDATE, DELETE ON table_name TO authenticated; + +-- Grant read-only to anonymous for public content +GRANT SELECT ON table_name TO anon; + +-- Grant usage on sequences +GRANT USAGE, SELECT ON SEQUENCE table_name_id_seq TO authenticated; diff --git a/skills/supabase-rls-policy-generator/references/rls-patterns.md b/skills/supabase-rls-policy-generator/references/rls-patterns.md new file mode 100644 index 0000000..08f0f70 --- /dev/null +++ b/skills/supabase-rls-policy-generator/references/rls-patterns.md @@ -0,0 +1,393 @@ +# RLS Policy Patterns + +This reference document provides common Row-Level Security patterns for Supabase applications. + +## Pattern 1: User Ownership + +Simple pattern where users can only access their own records. + +```sql +-- Enable RLS +ALTER TABLE posts ENABLE ROW LEVEL SECURITY; + +-- User can view own posts +CREATE POLICY "Users view own posts" + ON posts FOR SELECT + USING (auth.uid() = user_id); + +-- User can insert posts with their ID +CREATE POLICY "Users create own posts" + ON posts FOR INSERT + WITH CHECK (auth.uid() = user_id); + +-- User can update own posts +CREATE POLICY "Users update own posts" + ON posts FOR UPDATE + USING (auth.uid() = user_id) + WITH CHECK (auth.uid() = user_id); + +-- User can delete own posts +CREATE POLICY "Users delete own posts" + ON posts FOR DELETE + USING (auth.uid() = user_id); +``` + +## Pattern 2: Multi-Tenant Isolation + +Users can access all records within their tenant. + +```sql +-- Enable RLS +ALTER TABLE documents ENABLE ROW LEVEL SECURITY; + +-- Users can only see documents in their tenant +CREATE POLICY "Tenant isolation" + ON documents FOR ALL + USING ( + tenant_id IN ( + SELECT tenant_id FROM user_tenants + WHERE user_id = auth.uid() + ) + ); +``` + +## Pattern 3: Role-Based Access Control (RBAC) + +Different access levels based on user roles. + +```sql +-- Enable RLS +ALTER TABLE sensitive_data ENABLE ROW LEVEL SECURITY; + +-- Admins can view all records +CREATE POLICY "Admins view all" + ON sensitive_data FOR SELECT + USING (auth.jwt() ->> 'role' = 'admin'); + +-- Managers can view team records +CREATE POLICY "Managers view team" + ON sensitive_data FOR SELECT + USING ( + auth.jwt() ->> 'role' = 'manager' AND + team_id IN ( + SELECT team_id FROM user_teams + WHERE user_id = auth.uid() AND role = 'manager' + ) + ); + +-- Regular users can view own records +CREATE POLICY "Users view own" + ON sensitive_data FOR SELECT + USING (auth.uid() = user_id); +``` + +## Pattern 4: Public/Private Resources + +Some records are public, others are private. + +```sql +-- Enable RLS +ALTER TABLE articles ENABLE ROW LEVEL SECURITY; + +-- Everyone can view public articles +CREATE POLICY "Public articles readable" + ON articles FOR SELECT + USING (is_public = true); + +-- Authenticated users can view their private articles +CREATE POLICY "Private articles owner only" + ON articles FOR SELECT + USING (is_public = false AND auth.uid() = user_id); + +-- Only author can update +CREATE POLICY "Author can update" + ON articles FOR UPDATE + USING (auth.uid() = user_id); +``` + +## Pattern 5: Hierarchical Permissions + +Organization > Team > User hierarchy. + +```sql +-- Enable RLS +ALTER TABLE projects ENABLE ROW LEVEL SECURITY; + +-- Organization admins see all organization projects +CREATE POLICY "Org admins view all" + ON projects FOR SELECT + USING ( + organization_id IN ( + SELECT organization_id FROM user_organizations + WHERE user_id = auth.uid() AND role = 'admin' + ) + ); + +-- Team members see team projects +CREATE POLICY "Team members view team projects" + ON projects FOR SELECT + USING ( + team_id IN ( + SELECT team_id FROM user_teams + WHERE user_id = auth.uid() + ) + ); + +-- Project owner can always view +CREATE POLICY "Owner can view" + ON projects FOR SELECT + USING (auth.uid() = owner_id); +``` + +## Pattern 6: Shared Resources + +Resources shared with specific users. + +```sql +-- Enable RLS +ALTER TABLE files ENABLE ROW LEVEL SECURITY; + +-- Owner can view +CREATE POLICY "Owner can view files" + ON files FOR SELECT + USING (auth.uid() = owner_id); + +-- Shared users can view +CREATE POLICY "Shared users can view" + ON files FOR SELECT + USING ( + EXISTS ( + SELECT 1 FROM file_shares + WHERE file_id = files.id + AND user_id = auth.uid() + AND (expires_at IS NULL OR expires_at > NOW()) + ) + ); + +-- Public files viewable by all authenticated +CREATE POLICY "Public files viewable" + ON files FOR SELECT + USING (is_public = true); +``` + +## Pattern 7: JWT Claims-Based Access + +Access based on custom JWT claims. + +```sql +-- Enable RLS +ALTER TABLE premium_content ENABLE ROW LEVEL SECURITY; + +-- Premium users can view premium content +CREATE POLICY "Premium users access" + ON premium_content FOR SELECT + USING ( + (auth.jwt() -> 'app_metadata' ->> 'subscription_tier') IN ('premium', 'enterprise') + ); + +-- Organization-scoped access +CREATE POLICY "Organization scoped" + ON premium_content FOR SELECT + USING ( + organization_id = (auth.jwt() -> 'app_metadata' ->> 'organization_id')::uuid + ); +``` + +## Pattern 8: Time-Based Access + +Access that expires or becomes available at certain times. + +```sql +-- Enable RLS +ALTER TABLE scheduled_content ENABLE ROW LEVEL SECURITY; + +-- Content available after publish date +CREATE POLICY "Published content viewable" + ON scheduled_content FOR SELECT + USING ( + published_at IS NOT NULL AND + published_at <= NOW() AND + (expires_at IS NULL OR expires_at > NOW()) + ); + +-- Authors can always view +CREATE POLICY "Authors view all states" + ON scheduled_content FOR SELECT + USING (auth.uid() = author_id); +``` + +## Pattern 9: Cascading Permissions + +Permissions inherited from parent resources. + +```sql +-- Enable RLS on both tables +ALTER TABLE folders ENABLE ROW LEVEL SECURITY; +ALTER TABLE folder_items ENABLE ROW LEVEL SECURITY; + +-- Folder access based on ownership or sharing +CREATE POLICY "Folder access" + ON folders FOR SELECT + USING ( + auth.uid() = owner_id OR + EXISTS ( + SELECT 1 FROM folder_permissions + WHERE folder_id = folders.id AND user_id = auth.uid() + ) + ); + +-- Item access inherits from folder +CREATE POLICY "Item access via folder" + ON folder_items FOR SELECT + USING ( + folder_id IN ( + SELECT id FROM folders + WHERE auth.uid() = owner_id OR + EXISTS ( + SELECT 1 FROM folder_permissions + WHERE folder_id = folders.id AND user_id = auth.uid() + ) + ) + ); +``` + +## Pattern 10: Conditional Write Restrictions + +Different rules for reading vs writing. + +```sql +-- Enable RLS +ALTER TABLE moderated_content ENABLE ROW LEVEL SECURITY; + +-- Anyone authenticated can view approved content +CREATE POLICY "View approved content" + ON moderated_content FOR SELECT + USING (status = 'approved'); + +-- Users can insert content (starts as pending) +CREATE POLICY "Users can create content" + ON moderated_content FOR INSERT + WITH CHECK ( + auth.uid() = author_id AND + status = 'pending' + ); + +-- Only moderators can approve +CREATE POLICY "Moderators can approve" + ON moderated_content FOR UPDATE + USING (auth.jwt() ->> 'role' = 'moderator') + WITH CHECK ( + status IN ('approved', 'rejected') AND + (OLD.status != 'approved' OR auth.jwt() ->> 'role' = 'admin') + ); +``` + +## Helper Functions + +Reusable functions for complex RLS logic. + +```sql +-- Check if user has specific role +CREATE OR REPLACE FUNCTION auth.user_has_role(required_role TEXT) +RETURNS BOOLEAN AS $$ +BEGIN + RETURN (auth.jwt() ->> 'role') = required_role; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Check tenant membership +CREATE OR REPLACE FUNCTION auth.user_in_tenant(target_tenant_id UUID) +RETURNS BOOLEAN AS $$ +BEGIN + RETURN EXISTS ( + SELECT 1 FROM user_tenants + WHERE user_id = auth.uid() AND tenant_id = target_tenant_id + ); +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Check organization permission +CREATE OR REPLACE FUNCTION auth.user_can_access_org(org_id UUID, min_role TEXT DEFAULT 'member') +RETURNS BOOLEAN AS $$ +DECLARE + user_role TEXT; + role_hierarchy TEXT[] := ARRAY['member', 'manager', 'admin', 'owner']; + min_role_level INT; + user_role_level INT; +BEGIN + SELECT role INTO user_role + FROM user_organizations + WHERE user_id = auth.uid() AND organization_id = org_id; + + IF user_role IS NULL THEN + RETURN FALSE; + END IF; + + min_role_level := array_position(role_hierarchy, min_role); + user_role_level := array_position(role_hierarchy, user_role); + + RETURN user_role_level >= min_role_level; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; +``` + +## Performance Tips + +1. **Index Policy Columns**: Add indexes on columns used in policies +```sql +CREATE INDEX idx_posts_user_id ON posts(user_id); +CREATE INDEX idx_documents_tenant_id ON documents(tenant_id); +CREATE INDEX idx_files_owner_id ON files(owner_id); +``` + +2. **Avoid Subqueries**: Use helper functions for complex logic +```sql +-- Slow +USING ( + user_id IN ( + SELECT user_id FROM complex_query WHERE ... + ) +) + +-- Faster +USING (auth.check_user_access(user_id)) +``` + +3. **Materialized Views**: For complex hierarchical permissions +```sql +CREATE MATERIALIZED VIEW user_accessible_resources AS +SELECT user_id, resource_id, access_level +FROM ... -- complex joins + +-- Refresh periodically +REFRESH MATERIALIZED VIEW user_accessible_resources; +``` + +## Testing RLS Policies + +Always test policies with different user contexts: + +```sql +-- Test as specific user +SET request.jwt.claim.sub = 'user-uuid-here'; +SELECT * FROM table_name; + +-- Test as specific role +SET request.jwt.claim.role = 'admin'; +SELECT * FROM table_name; + +-- Test insert +INSERT INTO table_name (user_id, content) VALUES (auth.uid(), 'test'); + +-- Reset +RESET ALL; +``` + +## Common Pitfalls + +1. **Forgetting WITH CHECK**: INSERT/UPDATE policies need WITH CHECK clause +2. **Not enabling RLS**: ALTER TABLE ... ENABLE ROW LEVEL SECURITY is required +3. **Overly complex policies**: Keep policies simple, use helper functions +4. **Missing indexes**: Policy columns need indexes for performance +5. **Testing as superuser**: Superusers bypass RLS, test as regular users +6. **Not considering anon vs authenticated**: Different policies for public access diff --git a/skills/supabase-rls-policy-generator/references/supabase-auth.md b/skills/supabase-rls-policy-generator/references/supabase-auth.md new file mode 100644 index 0000000..5ec6809 --- /dev/null +++ b/skills/supabase-rls-policy-generator/references/supabase-auth.md @@ -0,0 +1,303 @@ +# Supabase Auth Reference + +This document provides reference information for Supabase authentication functions and JWT structure. + +## Core Auth Functions + +### auth.uid() + +Returns the UUID of the currently authenticated user. + +```sql +-- Usage in RLS policy +CREATE POLICY "User access own records" + ON table_name FOR SELECT + USING (auth.uid() = user_id); +``` + +**Returns**: UUID or NULL if unauthenticated + +**Common Uses**: +- User ownership checks +- Filtering user-specific data +- Inserting records with current user ID + +### auth.jwt() + +Returns the full JSON Web Token for the authenticated user. + +```sql +-- Access top-level claims +auth.jwt() ->> 'role' -- Returns: 'authenticated', 'anon', custom role + +-- Access nested app_metadata +auth.jwt() -> 'app_metadata' ->> 'subscription_tier' +auth.jwt() -> 'app_metadata' ->> 'organization_id' + +-- Access user_metadata +auth.jwt() -> 'user_metadata' ->> 'display_name' +``` + +**Returns**: JSONB object or NULL if unauthenticated + +## JWT Structure + +Standard Supabase JWT contains: + +```json +{ + "aud": "authenticated", + "exp": 1234567890, + "sub": "user-uuid", + "email": "user@example.com", + "phone": "", + "app_metadata": { + "provider": "email", + "providers": ["email"] + }, + "user_metadata": { + "display_name": "John Doe", + "avatar_url": "https://..." + }, + "role": "authenticated", + "aal": "aal1", + "amr": [ + { + "method": "password", + "timestamp": 1234567890 + } + ], + "session_id": "session-uuid" +} +``` + +### Custom Claims + +Add custom claims via database functions or auth hooks: + +```sql +-- Example: Add custom role from database +CREATE OR REPLACE FUNCTION auth.custom_access_token_hook(event jsonb) +RETURNS jsonb +LANGUAGE plpgsql +AS $$ +DECLARE + user_role TEXT; + org_id UUID; +BEGIN + -- Get user's role and organization + SELECT role, organization_id INTO user_role, org_id + FROM user_profiles + WHERE id = (event->>'user_id')::uuid; + + -- Add custom claims + event := jsonb_set(event, '{claims,role}', to_jsonb(user_role)); + event := jsonb_set(event, '{claims,organization_id}', to_jsonb(org_id::text)); + + RETURN event; +END; +$$; +``` + +Access custom claims in policies: + +```sql +-- Check custom role +CREATE POLICY "Admin access" + ON table_name FOR ALL + USING (auth.jwt() ->> 'role' = 'admin'); + +-- Check custom organization +CREATE POLICY "Organization access" + ON table_name FOR SELECT + USING ( + organization_id = (auth.jwt() ->> 'organization_id')::uuid + ); +``` + +## Common Patterns + +### Check Authentication Status + +```sql +-- Require authenticated user +USING (auth.uid() IS NOT NULL) + +-- Allow both authenticated and anonymous +USING (true) + +-- Authenticated users only +USING (auth.jwt() ->> 'role' = 'authenticated') +``` + +### Role-Based Access + +```sql +-- Simple role check +USING (auth.jwt() ->> 'role' = 'admin') + +-- Multiple roles +USING ( + auth.jwt() ->> 'role' IN ('admin', 'moderator') +) + +-- Custom role from app_metadata +USING ( + auth.jwt() -> 'app_metadata' ->> 'custom_role' = 'premium_user' +) +``` + +### Email Verification + +```sql +-- Require verified email +USING ( + (auth.jwt() -> 'email_confirmed_at') IS NOT NULL +) +``` + +### Multi-Factor Authentication + +```sql +-- Require MFA +USING ( + auth.jwt() ->> 'aal' = 'aal2' +) +``` + +## Session Management + +### Session ID + +Access current session: + +```sql +-- Filter by session +USING ( + session_id = (auth.jwt() ->> 'session_id')::uuid +) +``` + +### Authentication Method + +Check how user authenticated: + +```sql +-- Check for password authentication +USING ( + EXISTS ( + SELECT 1 FROM jsonb_array_elements(auth.jwt() -> 'amr') AS method + WHERE method ->> 'method' = 'password' + ) +) + +-- Check for OAuth +USING ( + EXISTS ( + SELECT 1 FROM jsonb_array_elements(auth.jwt() -> 'amr') AS method + WHERE method ->> 'method' = 'oauth' + ) +) +``` + +## User Metadata vs App Metadata + +### User Metadata +- User-controlled data +- Can be updated by user +- Store preferences, profile info +- Access: `auth.jwt() -> 'user_metadata'` + +### App Metadata +- Admin-controlled data +- Cannot be updated by user +- Store roles, permissions, org IDs +- Access: `auth.jwt() -> 'app_metadata'` + +## Security Considerations + +### Always Validate User ID + +```sql +-- GOOD: Enforce user ID on insert +CREATE POLICY "Insert own records" + ON table_name FOR INSERT + WITH CHECK (auth.uid() = user_id); + +-- BAD: Allow any user_id +CREATE POLICY "Insert records" + ON table_name FOR INSERT + WITH CHECK (true); +``` + +### Don't Trust Client-Provided IDs + +```sql +-- GOOD: Use auth.uid() directly +INSERT INTO posts (user_id, content) +VALUES (auth.uid(), 'content'); + +-- BAD: Trust client-provided user_id +INSERT INTO posts (user_id, content) +VALUES ($1, $2); -- Client could pass any user_id +``` + +### Use SECURITY DEFINER Carefully + +```sql +-- Mark function as SECURITY DEFINER only when necessary +CREATE FUNCTION check_permission() +RETURNS BOOLEAN +SECURITY DEFINER -- Runs with function owner's privileges +AS $$ + -- Be very careful here - this bypasses RLS +$$; +``` + +## Testing Auth Context + +Test policies with different auth states: + +```sql +-- Set user ID +SET request.jwt.claim.sub = 'user-uuid'; + +-- Set role +SET request.jwt.claim.role = 'admin'; + +-- Set custom claims +SET request.jwt.claims = '{"role": "admin", "organization_id": "org-uuid"}'; + +-- Reset +RESET request.jwt.claim.sub; +RESET request.jwt.claim.role; +RESET request.jwt.claims; +``` + +## Common Errors + +### "function auth.uid() does not exist" +- RLS is not enabled on the database +- Not using Supabase's auth schema + +### NULL when expecting user ID +- User is not authenticated +- Token has expired +- Policy executed outside auth context + +### "permission denied for relation" +- RLS is enabled but no policies match +- User doesn't have base GRANT permissions + +## Performance Tips + +1. **Cache JWT lookups**: Use helper functions to avoid repeated JWT parsing +2. **Index foreign keys**: Always index user_id, tenant_id, etc. +3. **Avoid complex JWT parsing**: Move complex logic to helper functions +4. **Use materialized views**: For complex role hierarchies + +## Additional Resources + +- Supabase Auth Documentation: https://supabase.com/docs/guides/auth +- Row Level Security Guide: https://supabase.com/docs/guides/auth/row-level-security +- JWT Customization: https://supabase.com/docs/guides/auth/custom-claims-and-role-based-access-control-rbac