@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
JavaScript
/**
* 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]) => `
.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