UNPKG

@rip-user/rls-debugger-mcp

Version:

AI-powered MCP server for debugging Supabase Row Level Security policies with Claude structured outputs

207 lines (188 loc) 6.93 kB
/** * Supabase RLS Documentation URLs and reference materials */ export const SUPABASE_RLS_DOCS = { main: 'https://supabase.com/docs/guides/database/postgres/row-level-security', performance: 'https://supabase.com/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv', simplified: 'https://supabase.com/docs/guides/troubleshooting/rls-simplified-BJTcS8', columnLevel: 'https://supabase.com/docs/guides/database/postgres/column-level-security', authHelpers: 'https://supabase.com/docs/guides/database/postgres/row-level-security#helper-functions' }; /** * Key RLS concepts and documentation snippets */ export const RLS_CONCEPTS = { policyTypes: { title: 'Policy Types: PERMISSIVE vs RESTRICTIVE', content: ` PERMISSIVE Policies (Default): - Use OR logic - if ANY PERMISSIVE policy grants access, the user can access the row - Most common type of policy - Example: "user owns record OR record is public" RESTRICTIVE Policies: - Use AND logic - ALL RESTRICTIVE policies must pass - Used to add additional conditions that must always be true - Example: "record must be active" (applies to all access) Combining Both: - If both types exist on a table, a row is accessible when: (at least one PERMISSIVE policy passes) AND (all RESTRICTIVE policies pass) `, ref: SUPABASE_RLS_DOCS.simplified }, policyCommands: { title: 'Policy Commands and Clauses', content: ` Commands (Operations): - ALL: Applies to SELECT, INSERT, UPDATE, DELETE - SELECT: Read operations - INSERT: Create operations - UPDATE: Modify operations - DELETE: Remove operations Clauses: - USING: Expression that determines which rows are visible/accessible - Used for: SELECT, UPDATE, DELETE - Example: USING (user_id = auth.uid()) - WITH CHECK: Expression that must be true for inserted/updated rows - Used for: INSERT, UPDATE - Example: WITH CHECK (status = 'active') `, ref: SUPABASE_RLS_DOCS.main }, authHelpers: { title: 'Supabase Auth Helper Functions', content: ` auth.uid(): - Returns the UUID of the currently authenticated user - Returns NULL if user is not authenticated - Usage: USING (user_id = auth.uid()) auth.jwt(): - Returns the full JWT token as JSONB - Access claims: (auth.jwt() ->> 'claim_name') - Example: (auth.jwt() ->> 'role') = 'admin' auth.email(): - Returns the email of the authenticated user - Usage: USING (email = auth.email()) `, ref: SUPABASE_RLS_DOCS.authHelpers }, performanceTips: { title: 'RLS Performance Best Practices', content: ` 1. Index columns used in policies: - If policy checks user_id = auth.uid(), index user_id - CREATE INDEX idx_table_user_id ON table(user_id); 2. Avoid complex JOINs in policies: - Each policy evaluation runs per row - Complex joins can slow down queries significantly 3. Use EXISTS for relationship checks: - More efficient than JOINs in policy expressions - Example: EXISTS (SELECT 1 FROM related WHERE related.id = main.related_id) 4. Be careful with function calls: - Functions in policies run for every row - Consider using indexes on function results 5. Test with EXPLAIN ANALYZE: - See how policies affect query performance - EXPLAIN ANALYZE SELECT * FROM table; `, ref: SUPABASE_RLS_DOCS.performance }, commonPatterns: { title: 'Common RLS Patterns', content: ` 1. User owns resource: CREATE POLICY "users_own_resource" ON table FOR ALL USING (user_id = auth.uid()); 2. Public read, authenticated write: CREATE POLICY "public_read" ON table FOR SELECT USING (true); CREATE POLICY "auth_write" ON table FOR INSERT WITH CHECK (auth.uid() IS NOT NULL); 3. Role-based access: CREATE POLICY "admin_access" ON table FOR ALL USING ( (auth.jwt() ->> 'role') = 'admin' ); 4. Team/organization access: CREATE POLICY "team_access" ON posts FOR SELECT USING ( EXISTS ( SELECT 1 FROM team_members WHERE team_members.user_id = auth.uid() AND team_members.team_id = posts.team_id ) ); 5. Time-based access: CREATE POLICY "published_content" ON posts FOR SELECT USING ( published_at <= now() ); `, ref: SUPABASE_RLS_DOCS.main }, troubleshooting: { title: 'Common RLS Issues', content: ` 1. "No rows returned" - RLS blocking access: - Check if RLS is enabled: ALTER TABLE table ENABLE ROW LEVEL SECURITY; - Verify policies exist: SELECT * FROM pg_policies WHERE tablename = 'table'; - Test with service role (bypasses RLS) to verify data exists 2. "Infinite loop" or timeout: - Policy might be checking a table that has policies checking back - Review EXISTS clauses for circular dependencies 3. "Permission denied": - Check if user has table-level permissions (GRANT SELECT, INSERT, etc.) - RLS policies only filter rows, they don't grant table access 4. Policy not applying: - Verify policy is for correct operation (SELECT vs INSERT vs UPDATE vs DELETE) - Check if policy is enabled - Ensure user role matches policy role (TO authenticated, TO anon, etc.) 5. Policy works for some users, not others: - Check auth.uid() returns expected value - Verify related records exist (for EXISTS clauses) - Test the policy condition directly in a query `, ref: SUPABASE_RLS_DOCS.simplified } }; /** * Get documentation for a specific concept */ export function getDocumentation(concept) { const doc = RLS_CONCEPTS[concept]; return `${doc.title}\n${doc.content}\n\nReference: ${doc.ref}`; } /** * Get all documentation as a formatted string */ export function getAllDocumentation() { return Object.entries(RLS_CONCEPTS) .map(([key, doc]) => `## ${doc.title}\n${doc.content}\n**Reference:** ${doc.ref}`) .join('\n\n---\n\n'); } /** * Search documentation for keywords */ export function searchDocumentation(query) { const results = []; const lowerQuery = query.toLowerCase(); Object.entries(RLS_CONCEPTS).forEach(([key, doc]) => { const titleMatch = doc.title.toLowerCase().includes(lowerQuery); const contentMatch = doc.content.toLowerCase().includes(lowerQuery); if (titleMatch || contentMatch) { // Extract relevant excerpt const lines = doc.content.split('\n').filter(l => l.trim()); const matchingLines = lines.filter(l => l.toLowerCase().includes(lowerQuery)); const excerpt = matchingLines.length > 0 ? matchingLines.slice(0, 3).join('\n') : lines.slice(0, 3).join('\n'); results.push({ concept: key, title: doc.title, excerpt: excerpt.trim(), ref: doc.ref }); } }); return results; } //# sourceMappingURL=docs.js.map