TheSimpleApp

supabase-mastery

0
0
# Install this skill:
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.