.claude/skills/supabase-troubleshooting/SKILL.md
Troubleshoot common Supabase issues including auth errors, RLS policies, connection problems, and performance. Use when debugging Supabase issues, fixing errors, or optimizing performance.
npx skillsauth add adaptationio/skrillz supabase-troubleshootingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Debug and fix common Supabase issues.
| Symptom | Likely Cause | |---------|--------------| | Empty data returned | RLS policies blocking access | | "Not authorized" | Missing or invalid auth token | | "JWT expired" | Token needs refresh | | Slow queries | Missing indexes or RLS subqueries | | Connection refused | Wrong URL or service down | | "Duplicate key" | Unique constraint violation | | Function timeout | Cold start or heavy computation |
// Check email format
const { data, error } = await supabase.auth.signInWithPassword({
email: email.toLowerCase().trim(), // Normalize email
password
})
// Check if user exists
const { data } = await supabase
.from('auth.users') // Admin only
.select('*')
.eq('email', email)
// Option 1: Resend confirmation
const { error } = await supabase.auth.resend({
type: 'signup',
email: '[email protected]'
})
// Option 2: Admin confirm (server-side)
await supabaseAdmin.auth.admin.updateUserById(userId, {
email_confirm: true
})
// Client auto-refreshes, but you can force it
const { data, error } = await supabase.auth.refreshSession()
// Listen for token refresh
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'TOKEN_REFRESHED') {
// Update any cached tokens
}
})
// Check storage configuration
const supabase = createClient(url, key, {
auth: {
persistSession: true, // Default is true
storage: localStorage // Or custom storage
}
})
// Check for multiple instances
// Only create one Supabase client per app
-- Check if RLS is enabled
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';
-- Check existing policies
SELECT * FROM pg_policies
WHERE tablename = 'your_table';
-- Test policy with your user ID
SET request.jwt.claims = '{"sub": "your-user-uuid", "role": "authenticated"}';
SELECT * FROM your_table;
// Check you're authenticated
const { data: { user } } = await supabase.auth.getUser()
console.log('Current user:', user?.id)
// Check the RLS policy requirements
// Common issue: auth.uid() doesn't match row's user_id
-- Enable detailed RLS debugging (temporary)
SET log_statement = 'all';
SET log_min_duration_statement = 0;
-- Test as specific user
SET request.jwt.claims = '{
"sub": "user-uuid",
"role": "authenticated",
"aal": "aal1"
}';
-- Run your query
SELECT * FROM posts;
-- Check auth functions
SELECT auth.uid();
SELECT auth.role();
SELECT auth.jwt();
-- Bad: Calls auth.uid() for each row
CREATE POLICY "slow" ON posts
USING (auth.uid() = user_id);
-- Good: Caches auth.uid() result
CREATE POLICY "fast" ON posts
USING ((SELECT auth.uid()) = user_id);
-- Add indexes for RLS columns
CREATE INDEX idx_posts_user_id ON posts(user_id);
// Use upsert instead of insert
const { data, error } = await supabase
.from('table')
.upsert({ id: existingId, ...values }, {
onConflict: 'id'
})
.select()
// Ensure referenced row exists
const { data: parent } = await supabase
.from('parent_table')
.select('id')
.eq('id', parentId)
.single()
if (!parent) {
// Create parent first or handle error
}
// Disable cache for specific queries
const { data } = await supabase
.from('table')
.select('*')
.eq('id', id)
.single()
.throwOnError() // Actually throws on error
// Force fresh data
const { data } = await supabase
.from('table')
.select('*', { head: false, count: 'exact' })
// Check Supabase URL
console.log('URL:', process.env.NEXT_PUBLIC_SUPABASE_URL)
// Check API key
console.log('Key prefix:', process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY?.slice(0, 20))
// Test connection
const { data, error } = await supabase.from('_health').select('*')
console.log('Health check:', { data, error })
# Check Docker is running
docker ps
# Check Supabase status
supabase status
# Restart if needed
supabase stop
supabase start
// Edge function must include CORS headers
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'
}
// Always handle OPTIONS preflight
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
const { data, error } = await supabase.storage
.from('bucket')
.upload('path', file)
if (error) {
// Check error type
if (error.message.includes('exceeded')) {
console.log('File too large')
} else if (error.message.includes('mime type')) {
console.log('Invalid file type')
} else if (error.message.includes('row-level security')) {
console.log('RLS policy blocking upload')
}
}
-- Check storage policies
SELECT * FROM pg_policies
WHERE tablename = 'objects'
AND schemaname = 'storage';
-- Common fix: Allow authenticated uploads to user folder
CREATE POLICY "Upload to own folder"
ON storage.objects FOR INSERT
TO authenticated
WITH CHECK (
bucket_id = 'uploads'
AND auth.uid()::text = (storage.foldername(name))[1]
);
// Check for blocking operations
// Bad: Long synchronous operation
const result = heavyComputation()
// Good: Keep operations async and fast
const result = await heavyComputationAsync()
// Check limits:
// - CPU time: 2 seconds
// - Wall clock: 150s (400s on Pro)
// Always wrap in try-catch
try {
const result = await riskyOperation()
return new Response(JSON.stringify({ data: result }))
} catch (error) {
console.error('Function error:', error)
return new Response(
JSON.stringify({ error: error.message }),
{ status: 500 }
)
}
# Check secrets are set
supabase secrets list
# Set missing secrets
supabase secrets set API_KEY=value
# Note: Changes apply immediately, no redeploy needed
// 1. Check table has realtime enabled
// SQL: ALTER PUBLICATION supabase_realtime ADD TABLE your_table;
// 2. Check subscription status
const channel = supabase
.channel('test')
.on('postgres_changes', { event: '*', schema: 'public', table: 'posts' }, callback)
.subscribe((status, err) => {
console.log('Subscription status:', status, err)
})
// 3. Check RLS allows SELECT
// Realtime respects RLS policies
// Don't call subscribe() twice on same channel
const channel = supabase.channel('my-channel')
channel.subscribe() // OK
channel.subscribe() // Error!
// Cleanup before resubscribing
await supabase.removeChannel(channel)
const newChannel = supabase.channel('my-channel').subscribe()
// Clean up channels on unmount
useEffect(() => {
const channel = supabase.channel('data')
.on('postgres_changes', {...}, callback)
.subscribe()
return () => {
supabase.removeChannel(channel) // Important!
}
}, [])
-- Use EXPLAIN ANALYZE to find bottlenecks
EXPLAIN ANALYZE SELECT * FROM posts WHERE user_id = 'uuid';
-- Add missing indexes
CREATE INDEX CONCURRENTLY idx_posts_user_id ON posts(user_id);
-- Check for sequential scans
SELECT relname, seq_scan, idx_scan
FROM pg_stat_user_tables
ORDER BY seq_scan DESC;
# Check connections
supabase inspect db role-connections --linked
# Use connection pooling for serverless
# Use transaction mode, not session mode
-- Avoid subqueries in hot path
-- Bad:
USING (user_id IN (SELECT user_id FROM team_members WHERE team_id = ...))
-- Good: Use security definer function
CREATE FUNCTION get_user_teams()
RETURNS SETOF uuid
LANGUAGE sql STABLE SECURITY DEFINER
AS $$ SELECT team_id FROM team_members WHERE user_id = auth.uid() $$;
USING (team_id IN (SELECT get_user_teams()))
| Code | Description | Solution |
|------|-------------|----------|
| PGRST116 | No rows found (single expected) | Use maybeSingle() instead of single() |
| PGRST205 | Table not found in schema cache | Check table name spelling, verify table exists |
| PGRST301 | Multiple rows (single expected) | Add unique constraint or better filter |
| Code | Description | Solution |
|------|-------------|----------|
| 22P02 | Invalid input syntax (e.g., bad UUID) | Validate input format before query |
| 23503 | Foreign key violation | Ensure parent row exists first |
| 23505 | Unique constraint violation | Use upsert or check existence |
| 42501 | Insufficient privilege (RLS) | Fix RLS policies or use service role |
| 42703 | Column does not exist | Check column name spelling |
| 42P01 | Relation (table) doesn't exist | Check table name and schema |
| 08P01 | Protocol violation | Check query syntax |
| Error | Description | Solution |
|-------|-------------|----------|
| Invalid API key | Wrong or missing apikey header | Check SUPABASE_ANON_KEY |
| JWT expired | Access token expired | Call refreshSession() |
| Invalid login credentials | Wrong email/password | Verify credentials |
development
Setup secure web-based terminal access to WSL2 from mobile/tablet via ttyd + ngrok/Cloudflare/Tailscale. One-command install, start, stop, status. Use when you need remote terminal access, web terminal, browser-based shell, or mobile access to WSL2 environment.
development
Complete development workflows where Claude writes the code while Gemini and Codex provide research, planning, reviews, and different perspectives. Claude remains the main developer. Use for complex projects requiring expert planning and multi-perspective reviews.
development
Systematic progress tracking for skill development. Manages task states (pending/in_progress/completed), updates in real-time, reports progress, identifies blockers, and maintains momentum. Use when tracking skill development, coordinating work, or reporting progress.
testing
Comprehensive testing workflow orchestrating functional testing, example validation, integration testing, and usability assessment. Sequential workflow for complete skill testing from examples through scenarios to integration validation. Use when conducting thorough testing, pre-deployment validation, ensuring skill functionality, or comprehensive quality checks.