Manage Apple Reminders via the `remindctl` CLI on macOS (list, add, edit, complete, delete)....
npx skills add TheSimpleApp/agent-skills --skill "supabase-mastery"
Install specific skill from multi-skill repository
# Description
Supabase deep optimization for performance, security, and best practices. Covers PostgreSQL queries, RLS policies, real-time, and edge functions. Use when building or optimizing Supabase projects.
# SKILL.md
name: supabase-mastery
description: Supabase deep optimization for performance, security, and best practices. Covers PostgreSQL queries, RLS policies, real-time, and edge functions. Use when building or optimizing Supabase projects.
license: MIT
metadata:
author: thesimpleapp
version: "1.0"
Supabase Mastery
Deep optimization for Supabase projects - database, auth, storage, and edge functions.
Integration via Rube (Preferred)
Use Rube for all Supabase operations - no per-project API keys needed.
Quick Start
# List your projects
SUPABASE_LIST_ALL_PROJECTS
# List tables in a project
SUPABASE_LIST_TABLES project_ref="tzuhmsgldrzqjoqlimju"
# Run SQL query
SUPABASE_BETA_RUN_SQL_QUERY
ref="tzuhmsgldrzqjoqlimju"
query="SELECT * FROM users LIMIT 10"
read_only=true
# Get table schema
SUPABASE_GET_TABLE_SCHEMAS
project_ref="tzuhmsgldrzqjoqlimju"
table_names=["users", "orders"]
Project Detection
Rube auto-detects project from:
1. CLAUDE.md project_ref field
2. Folder name matching project name
3. Ask user if ambiguous
See /rube-hub for full integration details.
Performance Impact Levels
| Level | Improvement | Focus Areas |
|---|---|---|
| CRITICAL | 10-100x | Missing indexes, connection exhaustion |
| HIGH | 5-20x | Wrong index types, poor partitioning |
| MEDIUM-HIGH | 2-5x | N+1 queries, RLS optimization |
| MEDIUM | 1.5-3x | Redundant indexes, stale statistics |
Database Optimization
Index Strategy
-- ❌ Missing index on frequently queried column
SELECT * FROM orders WHERE user_id = 'xxx';
-- ✅ Add index
CREATE INDEX idx_orders_user_id ON orders(user_id);
-- ✅ Partial index for common queries
CREATE INDEX idx_orders_pending ON orders(user_id)
WHERE status = 'pending';
-- ✅ Composite index for multi-column queries
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
Query Optimization
-- ❌ N+1 Query Pattern
-- First query: SELECT * FROM users
-- Then for each user: SELECT * FROM orders WHERE user_id = ?
-- ✅ Single query with join
SELECT u.*,
COALESCE(json_agg(o.*) FILTER (WHERE o.id IS NOT NULL), '[]') as orders
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id;
-- ❌ SELECT * when you only need specific columns
SELECT * FROM users;
-- ✅ Select only needed columns
SELECT id, name, email FROM users;
-- ❌ Loading all records for pagination
SELECT * FROM products ORDER BY created_at;
-- ✅ Cursor-based pagination
SELECT * FROM products
WHERE created_at < $1
ORDER BY created_at DESC
LIMIT 20;
Connection Pooling
// Use connection pooler for serverless
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
// For Edge Functions / Serverless
// Use the pooler URL (port 6543) not direct (port 5432)
const connectionString = `postgresql://postgres:[password]@[host]:6543/postgres`;
EXPLAIN ANALYZE
-- Always check query plans for slow queries
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE user_id = 'xxx'
AND created_at > NOW() - INTERVAL '30 days';
-- Look for:
-- - Seq Scan (might need index)
-- - High row estimates
-- - Slow nested loops
Row Level Security (RLS)
Policy Patterns
-- Enable RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Users can only see their own orders
CREATE POLICY "Users can view own orders"
ON orders FOR SELECT
USING (auth.uid() = user_id);
-- Users can insert their own orders
CREATE POLICY "Users can create own orders"
ON orders FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Users can update their own pending orders
CREATE POLICY "Users can update own pending orders"
ON orders FOR UPDATE
USING (auth.uid() = user_id AND status = 'pending')
WITH CHECK (auth.uid() = user_id);
-- Admin role bypass
CREATE POLICY "Admins can do anything"
ON orders FOR ALL
USING (
EXISTS (
SELECT 1 FROM profiles
WHERE profiles.id = auth.uid()
AND profiles.role = 'admin'
)
);
RLS Performance
-- ❌ Slow: Subquery in every policy check
USING (
user_id IN (SELECT id FROM users WHERE org_id = auth.jwt()->>'org_id')
);
-- ✅ Fast: Direct comparison with JWT claims
USING (
(auth.jwt()->>'org_id')::uuid = org_id
);
-- ✅ Index for RLS column
CREATE INDEX idx_orders_org_id ON orders(org_id);
Real-time Optimization
Channel Management
// ❌ Too many subscriptions
orders.forEach(order => {
supabase.channel(`order-${order.id}`).subscribe();
});
// ✅ Single subscription with filter
const channel = supabase
.channel('orders')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'orders',
filter: `user_id=eq.${userId}`,
},
(payload) => handleOrderChange(payload)
)
.subscribe();
// Always cleanup
useEffect(() => {
return () => {
supabase.removeChannel(channel);
};
}, []);
Broadcast for Presence
// Use broadcast for presence, not database
const room = supabase.channel('room-1', {
config: {
presence: {
key: userId,
},
},
});
room
.on('presence', { event: 'sync' }, () => {
const state = room.presenceState();
setOnlineUsers(Object.keys(state));
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await room.track({ online_at: new Date().toISOString() });
}
});
Edge Functions
Best Practices
// supabase/functions/process-order/index.ts
import { serve } from 'https://deno.land/[email protected]/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
serve(async (req) => {
// Handle CORS preflight
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
// Create client with user's JWT
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{
global: {
headers: { Authorization: req.headers.get('Authorization')! },
},
}
);
// Verify user
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
// Process request
const { orderId } = await req.json();
const { data, error } = await supabase
.from('orders')
.update({ status: 'processed' })
.eq('id', orderId)
.eq('user_id', user.id) // Always filter by user
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify(data), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
});
Storage Optimization
// Generate signed URLs for private files
const { data } = await supabase.storage
.from('documents')
.createSignedUrl('file.pdf', 3600); // 1 hour expiry
// Transform images on the fly
const imageUrl = supabase.storage
.from('avatars')
.getPublicUrl('user.jpg', {
transform: {
width: 200,
height: 200,
resize: 'cover',
},
});
// Upload with proper content type
await supabase.storage
.from('uploads')
.upload(path, file, {
contentType: file.type,
cacheControl: '3600',
});
Monitoring & Debugging
Query Performance
-- Find slow queries
SELECT
query,
calls,
total_time,
mean_time,
rows
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 10;
-- Check index usage
SELECT
schemaname,
tablename,
indexname,
idx_scan,
idx_tup_read
FROM pg_stat_user_indexes
ORDER BY idx_scan DESC;
Dashboard Queries
Check in Supabase Dashboard:
- Database → Query Performance
- Database → Index Advisor
- Logs → API Logs (for slow requests)
Security Checklist
□ RLS enabled on all tables
□ Service role key only in backend
□ Anon key only has minimal permissions
□ All policies tested
□ No secrets in client code
□ Webhook signatures verified
□ Rate limiting configured
□ Auth redirects to safe URLs only
Schema Design
Timestamps
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
total DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Auto-update updated_at
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER orders_updated_at
BEFORE UPDATE ON orders
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();
Soft Deletes
ALTER TABLE orders ADD COLUMN deleted_at TIMESTAMPTZ;
-- RLS policy excludes deleted
CREATE POLICY "Exclude deleted orders"
ON orders FOR SELECT
USING (deleted_at IS NULL);
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.