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.