// Advanced Supabase integration specialist for Auth, Database (PostgreSQL/RLS), Storage, Realtime, Edge Functions, and AI/Vector features. Use when implementing Supabase features, debugging Supabase issues, setting up RLS policies, creating database schemas, building auth flows, optimizing Supabase queries, migrating to Supabase, or architecting Supabase-based applications. Invoke for Supabase client setup, type generation, migration creation, performance tuning, security audits, or Supabase best practices. Handles Next.js, React, Vue, Svelte, and server-side integrations.
| name | supabase-expert |
| description | Advanced Supabase integration specialist for Auth, Database (PostgreSQL/RLS), Storage, Realtime, Edge Functions, and AI/Vector features. Use when implementing Supabase features, debugging Supabase issues, setting up RLS policies, creating database schemas, building auth flows, optimizing Supabase queries, migrating to Supabase, or architecting Supabase-based applications. Invoke for Supabase client setup, type generation, migration creation, performance tuning, security audits, or Supabase best practices. Handles Next.js, React, Vue, Svelte, and server-side integrations. |
| allowed-tools | Read, Write, Edit, Bash, Grep, Glob |
| model | sonnet |
This is a comprehensive, production-grade skill for working with Supabase across all aspects of modern application development. It provides:
This skill leverages the complete Supabase documentation (2,190 pages) to provide accurate, up-to-date guidance across:
Base Path: /Users/zach/Documents/cc-skills/docs/supabase/
When a request comes in, use a multi-stage search approach:
# Identify relevant category
grep -r "keyword" /Users/zach/Documents/cc-skills/docs/supabase/guides/ -l | head -10
# Search within specific category with context
grep -r -B 2 -A 5 "specific pattern" /Users/zach/Documents/cc-skills/docs/supabase/guides/[category]/ --include="*.txt"
# Find related documentation
grep -r "related_term_1\|related_term_2\|related_term_3" /Users/zach/Documents/cc-skills/docs/supabase/ -l
# Search API reference for specific methods
grep -r "method_name" /Users/zach/Documents/cc-skills/docs/supabase/reference/ -l
Before providing any solution, analyze:
Technical Requirements:
Context Detection:
# Check if project uses Next.js
[ -f "next.config.js" ] && echo "Next.js detected"
# Check if TypeScript
[ -f "tsconfig.json" ] && echo "TypeScript project"
# Check existing Supabase setup
grep -r "createClient" . --include="*.{ts,js,tsx,jsx}" | head -5
Existing Code Analysis:
Execute multi-stage search strategy:
# Example: Searching for auth implementation
# Stage 1: Find all auth-related docs
AUTH_DOCS=$(grep -r "authentication\|sign.*in\|auth\..*" /Users/zach/Documents/cc-skills/docs/supabase/guides/auth/ -l)
# Stage 2: Narrow to specific method (e.g., Google OAuth)
OAUTH_DOCS=$(echo "$AUTH_DOCS" | xargs grep -l "google\|oauth")
# Stage 3: Read the most relevant docs
# (Read top 3 most relevant files)
Provide implementations that match the user's context:
For Next.js App Router:
For Next.js Pages Router:
For Client-Only React:
For Server-Side (Node/Deno/Bun):
Every code example should include:
✅ Complete TypeScript types ✅ Comprehensive error handling ✅ Loading states ✅ Edge cases handled ✅ Performance optimizations ✅ Security considerations ✅ Testing examples ✅ Monitoring/logging hooks
Provide:
.claude/skills/supabase-expert/patterns/
Scenario: Multi-tenant SaaS with organization-based access control
Search Strategy:
grep -r "multi.*tenant\|organization\|team.*access" /Users/zach/Documents/cc-skills/docs/supabase/guides/ -l
grep -r "row.*level.*security.*tenant" /Users/zach/Documents/cc-skills/docs/supabase/guides/database/ --include="*.txt" -A 10
Implementation:
-- Organization table
CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Membership table with roles
CREATE TABLE organization_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'member')),
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(organization_id, user_id)
);
-- Projects table (tenant data)
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
name TEXT NOT NULL,
created_by UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Enable RLS on all tables
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
ALTER TABLE organization_members ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Helper function to check membership
CREATE OR REPLACE FUNCTION is_organization_member(org_id UUID)
RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (
SELECT 1 FROM organization_members
WHERE organization_id = org_id
AND user_id = auth.uid()
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Helper function to check role
CREATE OR REPLACE FUNCTION get_user_role(org_id UUID)
RETURNS TEXT AS $$
BEGIN
RETURN (
SELECT role FROM organization_members
WHERE organization_id = org_id
AND user_id = auth.uid()
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- RLS Policies for organizations
CREATE POLICY "Users can view their organizations"
ON organizations FOR SELECT
USING (is_organization_member(id));
-- RLS Policies for projects
CREATE POLICY "Organization members can view projects"
ON projects FOR SELECT
USING (is_organization_member(organization_id));
CREATE POLICY "Admins and owners can insert projects"
ON projects FOR INSERT
WITH CHECK (
get_user_role(organization_id) IN ('admin', 'owner')
AND auth.uid() = created_by
);
CREATE POLICY "Admins and owners can update projects"
ON projects FOR UPDATE
USING (get_user_role(organization_id) IN ('admin', 'owner'));
CREATE POLICY "Owners can delete projects"
ON projects FOR DELETE
USING (get_user_role(organization_id) = 'owner');
-- Indexes for performance
CREATE INDEX idx_org_members_user ON organization_members(user_id);
CREATE INDEX idx_org_members_org ON organization_members(organization_id);
CREATE INDEX idx_projects_org ON projects(organization_id);
TypeScript Client Usage:
// types/database.ts
export interface Organization {
id: string
name: string
created_at: string
}
export interface OrganizationMember {
id: string
organization_id: string
user_id: string
role: 'owner' | 'admin' | 'member'
created_at: string
}
export interface Project {
id: string
organization_id: string
name: string
created_by: string
created_at: string
}
// lib/supabase/organizations.ts
import { createClient } from '@supabase/supabase-js'
import type { Database } from '@/types/supabase'
export class OrganizationService {
constructor(private supabase: ReturnType<typeof createClient<Database>>) {}
async getOrganizations() {
const { data, error } = await this.supabase
.from('organizations')
.select(`
*,
organization_members!inner(role)
`)
if (error) throw error
return data
}
async getProjects(organizationId: string) {
const { data, error} = await this.supabase
.from('projects')
.select('*')
.eq('organization_id', organizationId)
.order('created_at', { ascending: false })
if (error) throw error
return data
}
async createProject(organizationId: string, name: string) {
const { data: { user } } = await this.supabase.auth.getUser()
if (!user) throw new Error('Not authenticated')
const { data, error } = await this.supabase
.from('projects')
.insert({
organization_id: organizationId,
name,
created_by: user.id
})
.select()
.single()
if (error) throw error
return data
}
}
Search Strategy:
grep -r "custom.*claims\|jwt.*metadata\|user.*metadata" /Users/zach/Documents/cc-skills/docs/supabase/guides/auth/ -l
grep -r "auth.*hooks\|hook.*send" /Users/zach/Documents/cc-skills/docs/supabase/guides/auth/ --include="*.txt" -A 10
Implementation:
// lib/auth/custom-claims.ts
import { createClient } from '@supabase/supabase-js'
export interface UserClaims {
role: 'admin' | 'user' | 'moderator'
organization_id?: string
permissions: string[]
}
export async function setUserClaims(
userId: string,
claims: UserClaims
): Promise<void> {
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY! // Service role required
)
const { error } = await supabase.auth.admin.updateUserById(
userId,
{
app_metadata: { claims }
}
)
if (error) throw error
}
export async function getUserClaims(userId: string): Promise<UserClaims | null> {
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
const { data, error } = await supabase.auth.admin.getUserById(userId)
if (error) throw error
return data.user.app_metadata.claims as UserClaims || null
}
// Middleware for Next.js App Router
// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
const response = NextResponse.next()
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value
},
set(name: string, value: string, options: any) {
response.cookies.set({ name, value, ...options })
},
remove(name: string, options: any) {
response.cookies.set({ name, value: '', ...options })
},
},
}
)
const { data: { session } } = await supabase.auth.getSession()
// Check if accessing admin route
if (request.nextUrl.pathname.startsWith('/admin')) {
if (!session) {
return NextResponse.redirect(new URL('/login', request.url))
}
const claims = session.user.app_metadata.claims as UserClaims
if (claims?.role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url))
}
}
return response
}
export const config = {
matcher: ['/admin/:path*', '/dashboard/:path*']
}
Search Strategy:
grep -r "presence\|broadcast\|realtime.*channel" /Users/zach/Documents/cc-skills/docs/supabase/guides/realtime/ -l
Implementation:
// hooks/usePresence.ts
import { useEffect, useState } from 'react'
import { createClient } from '@supabase/supabase-js'
import type { RealtimeChannel } from '@supabase/supabase-js'
interface PresenceState {
[key: string]: {
user_id: string
username: string
online_at: string
}[]
}
export function usePresence(roomId: string) {
const [presenceState, setPresenceState] = useState<PresenceState>({})
const [channel, setChannel] = useState<RealtimeChannel | null>(null)
useEffect(() => {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
const presenceChannel = supabase.channel(`room:${roomId}`, {
config: {
presence: {
key: 'user_id',
},
},
})
presenceChannel
.on('presence', { event: 'sync' }, () => {
const state = presenceChannel.presenceState()
setPresenceState(state)
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('User joined:', key, newPresences)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('User left:', key, leftPresences)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
const { data: { user } } = await supabase.auth.getUser()
if (user) {
await presenceChannel.track({
user_id: user.id,
username: user.email,
online_at: new Date().toISOString(),
})
}
}
})
setChannel(presenceChannel)
return () => {
presenceChannel.unsubscribe()
}
}, [roomId])
const sendBroadcast = async (event: string, payload: any) => {
if (channel) {
await channel.send({
type: 'broadcast',
event,
payload,
})
}
}
return {
presenceState,
onlineUsers: Object.values(presenceState).flat(),
sendBroadcast,
}
}
// Component usage
export function CollaborativeEditor({ documentId }: { documentId: string }) {
const { onlineUsers, sendBroadcast } = usePresence(documentId)
const handleCursorMove = (position: { x: number; y: number }) => {
sendBroadcast('cursor_move', position)
}
return (
<div>
<div className="online-users">
{onlineUsers.map(user => (
<div key={user.user_id}>
{user.username} (online)
</div>
))}
</div>
{/* Editor component */}
</div>
)
}
Search Strategy:
grep -r "vector\|embedding\|pgvector\|similarity" /Users/zach/Documents/cc-skills/docs/supabase/guides/ai/ -l
grep -r "semantic.*search\|vector.*search" /Users/zach/Documents/cc-skills/docs/supabase/guides/ai/ --include="*.txt" -A 10
Implementation:
-- Enable pgvector extension
CREATE EXTENSION IF NOT EXISTS vector;
-- Documents table with embeddings
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content TEXT NOT NULL,
embedding vector(1536), -- OpenAI ada-002 dimension
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Create index for vector similarity search
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- Function for similarity search
CREATE OR REPLACE FUNCTION match_documents(
query_embedding vector(1536),
match_threshold float,
match_count int
)
RETURNS TABLE (
id UUID,
content TEXT,
metadata JSONB,
similarity float
) LANGUAGE sql STABLE AS $$
SELECT
id,
content,
metadata,
1 - (embedding <=> query_embedding) AS similarity
FROM documents
WHERE 1 - (embedding <=> query_embedding) > match_threshold
ORDER BY embedding <=> query_embedding
LIMIT match_count;
$$;
TypeScript Implementation:
// lib/ai/embeddings.ts
import { createClient } from '@supabase/supabase-js'
import OpenAI from 'openai'
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
export async function generateEmbedding(text: string): Promise<number[]> {
const response = await openai.embeddings.create({
model: 'text-embedding-ada-002',
input: text,
})
return response.data[0].embedding
}
export async function addDocument(
content: string,
metadata: Record<string, any> = {}
): Promise<void> {
const embedding = await generateEmbedding(content)
const { error } = await supabase
.from('documents')
.insert({
content,
embedding,
metadata,
})
if (error) throw error
}
export async function searchDocuments(
query: string,
matchThreshold: number = 0.78,
matchCount: number = 10
) {
const queryEmbedding = await generateEmbedding(query)
const { data, error } = await supabase.rpc('match_documents', {
query_embedding: queryEmbedding,
match_threshold: matchThreshold,
match_count: matchCount,
})
if (error) throw error
return data
}
// RAG (Retrieval Augmented Generation) implementation
export async function ragQuery(userQuestion: string): Promise<string> {
// 1. Get relevant documents
const relevantDocs = await searchDocuments(userQuestion, 0.78, 5)
// 2. Build context from documents
const context = relevantDocs
.map(doc => doc.content)
.join('\n\n')
// 3. Generate response with OpenAI
const response = await openai.chat.completions.create({
model: 'gpt-4',
messages: [
{
role: 'system',
content: 'You are a helpful assistant. Answer questions based on the provided context.',
},
{
role: 'user',
content: `Context:\n${context}\n\nQuestion: ${userQuestion}`,
},
],
})
return response.choices[0].message.content || ''
}
Search Strategy:
grep -r "edge.*function\|deno\|background.*job" /Users/zach/Documents/cc-skills/docs/supabase/guides/functions/ -l
Implementation:
// supabase/functions/process-payment/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import Stripe from 'https://esm.sh/stripe@14.0.0'
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY') || '', {
apiVersion: '2023-10-16',
})
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
interface PaymentRequest {
amount: number
currency: string
userId: string
organizationId: string
}
serve(async (req) => {
try {
// 1. Authenticate request
const authHeader = req.headers.get('Authorization')
if (!authHeader) {
return new Response(
JSON.stringify({ error: 'Missing authorization' }),
{ status: 401 }
)
}
const { data: { user }, error: authError } = await supabase.auth.getUser(
authHeader.replace('Bearer ', '')
)
if (authError || !user) {
return new Response(
JSON.stringify({ error: 'Invalid authorization' }),
{ status: 401 }
)
}
// 2. Parse request body
const body: PaymentRequest = await req.json()
// 3. Create Stripe payment intent
const paymentIntent = await stripe.paymentIntents.create({
amount: body.amount,
currency: body.currency,
metadata: {
user_id: body.userId,
organization_id: body.organizationId,
},
})
// 4. Store payment record in database
const { error: dbError } = await supabase
.from('payments')
.insert({
user_id: body.userId,
organization_id: body.organizationId,
stripe_payment_intent_id: paymentIntent.id,
amount: body.amount,
currency: body.currency,
status: 'pending',
})
if (dbError) throw dbError
// 5. Return client secret
return new Response(
JSON.stringify({
clientSecret: paymentIntent.client_secret,
paymentIntentId: paymentIntent.id,
}),
{
headers: { 'Content-Type': 'application/json' },
status: 200,
}
)
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{
headers: { 'Content-Type': 'application/json' },
status: 500,
}
)
}
})
// lib/supabase/server.ts - Server Components
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
import type { Database } from '@/types/supabase'
export function createClient() {
const cookieStore = cookies()
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
try {
cookieStore.set({ name, value, ...options })
} catch (error) {
// Server Component can't set cookies
}
},
remove(name: string, options: CookieOptions) {
try {
cookieStore.set({ name, value: '', ...options })
} catch (error) {
// Server Component can't delete cookies
}
},
},
}
)
}
// lib/supabase/client.ts - Client Components
import { createBrowserClient } from '@supabase/ssr'
import type { Database } from '@/types/supabase'
export function createClient() {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
// app/auth/callback/route.ts - Auth callback handler
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
const next = searchParams.get('next') ?? '/dashboard'
if (code) {
const supabase = createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
return NextResponse.redirect(`${origin}${next}`)
}
}
return NextResponse.redirect(`${origin}/auth/auth-code-error`)
}
// app/dashboard/page.tsx - Server Component with data fetching
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
const supabase = createClient()
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) {
redirect('/login')
}
const { data: projects } = await supabase
.from('projects')
.select('*')
.order('created_at', { ascending: false })
return (
<div>
<h1>Welcome {user.email}</h1>
<ProjectsList projects={projects || []} />
</div>
)
}
// app/actions/projects.ts - Server Actions
'use server'
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
export async function createProject(formData: FormData) {
const supabase = createClient()
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) {
return { error: 'Not authenticated' }
}
const name = formData.get('name') as string
const organizationId = formData.get('organizationId') as string
const { data, error } = await supabase
.from('projects')
.insert({
name,
organization_id: organizationId,
created_by: user.id,
})
.select()
.single()
if (error) {
return { error: error.message }
}
revalidatePath('/dashboard')
return { data }
}
export async function deleteProject(projectId: string) {
const supabase = createClient()
const { error } = await supabase
.from('projects')
.delete()
.eq('id', projectId)
if (error) {
return { error: error.message }
}
revalidatePath('/dashboard')
return { success: true }
}
Search:
grep -r "connection.*pool\|supavisor\|serverless" /Users/zach/Documents/cc-skills/docs/supabase/guides/ -l
Implementation:
// lib/supabase/connection-pool.ts
import { createClient } from '@supabase/supabase-js'
// Use connection pooler for serverless environments
const POOLER_URL = process.env.SUPABASE_URL?.replace(
'.supabase.co',
'.pooler.supabase.com'
)
export function createPooledClient() {
return createClient(
POOLER_URL || process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{
db: {
schema: 'public',
},
auth: {
persistSession: false,
},
}
)
}
// Use in API routes
export async function GET(request: Request) {
const supabase = createPooledClient()
const { data, error } = await supabase
.from('large_table')
.select('*')
.limit(1000)
// Connection automatically returned to pool
return Response.json({ data, error })
}
// lib/supabase/optimized-queries.ts
// ❌ BAD: Fetches all columns and data
const { data } = await supabase
.from('users')
.select('*')
// ✅ GOOD: Select only needed columns
const { data } = await supabase
.from('users')
.select('id, email, username')
// ✅ GOOD: Use pagination
const { data, error, count } = await supabase
.from('users')
.select('*', { count: 'exact' })
.range(0, 49) // First 50 items
.order('created_at', { ascending: false })
// ✅ GOOD: Use joins efficiently
const { data } = await supabase
.from('posts')
.select(`
id,
title,
author:users!inner(id, username),
comments(count)
`)
.eq('published', true)
.limit(10)
// ✅ GOOD: Use indexes (ensure indexes exist)
const { data } = await supabase
.from('posts')
.select('*')
.eq('user_id', userId) // Indexed column
.eq('status', 'published') // Indexed column
// lib/cache/supabase-cache.ts
import { createClient } from '@supabase/supabase-js'
import { unstable_cache } from 'next/cache'
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
)
// Cache queries with Next.js cache
export const getCachedProjects = unstable_cache(
async (organizationId: string) => {
const { data, error } = await supabase
.from('projects')
.select('*')
.eq('organization_id', organizationId)
.order('created_at', { ascending: false })
if (error) throw error
return data
},
['projects'], // Cache key
{
revalidate: 60, // Revalidate every 60 seconds
tags: ['projects'], // For manual revalidation
}
)
// Use with revalidation
import { revalidateTag } from 'next/cache'
export async function createProject(name: string, orgId: string) {
const { data, error } = await supabase
.from('projects')
.insert({ name, organization_id: orgId })
.select()
.single()
if (!error) {
revalidateTag('projects') // Invalidate cache
}
return { data, error }
}
// scripts/security-audit.ts
interface SecurityAudit {
checks: SecurityCheck[]
passed: boolean
failures: string[]
}
interface SecurityCheck {
name: string
passed: boolean
message?: string
}
export async function auditSupabaseSecurity(): Promise<SecurityAudit> {
const checks: SecurityCheck[] = []
// 1. Check for exposed service role key
const serviceKeyCheck = !process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY
checks.push({
name: 'Service Role Key Security',
passed: serviceKeyCheck,
message: serviceKeyCheck
? 'Service role key not exposed in public env vars'
: '❌ CRITICAL: Service role key exposed in public env vars!',
})
// 2. Check RLS is enabled on all tables
// (Run in Supabase SQL editor or via service role client)
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
const { data: tables } = await supabase.rpc('check_rls_enabled')
const rlsCheck = tables?.every(t => t.rls_enabled)
checks.push({
name: 'RLS Enabled on All Tables',
passed: rlsCheck || false,
message: rlsCheck
? 'All tables have RLS enabled'
: '⚠️ Some tables missing RLS policies',
})
// 3. Check for SQL injection vulnerabilities
// Scan codebase for string concatenation in queries
const sqlInjectionCheck = true // Implement static analysis
checks.push({
name: 'No SQL Injection Risks',
passed: sqlInjectionCheck,
})
// 4. Check HTTPS only
const httpsCheck = process.env.SUPABASE_URL?.startsWith('https://')
checks.push({
name: 'HTTPS Only',
passed: httpsCheck || false,
message: httpsCheck ? 'Using HTTPS' : '❌ Not using HTTPS!',
})
const passed = checks.every(check => check.passed)
const failures = checks
.filter(check => !check.passed)
.map(check => check.message || check.name)
return { checks, passed, failures }
}
// tests/rls-policies.test.ts
import { createClient } from '@supabase/supabase-js'
import { describe, it, expect, beforeAll } from 'vitest'
describe('RLS Policies', () => {
let supabase: ReturnType<typeof createClient>
let testUserId: string
beforeAll(async () => {
// Create test user
supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
const { data: { user } } = await supabase.auth.admin.createUser({
email: 'test@example.com',
password: 'test-password-123',
email_confirm: true,
})
testUserId = user!.id
})
it('should allow users to read their own data', async () => {
// Sign in as test user
const { data: { session } } = await supabase.auth.signInWithPassword({
email: 'test@example.com',
password: 'test-password-123',
})
const userClient = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{
global: {
headers: {
Authorization: `Bearer ${session!.access_token}`,
},
},
}
)
const { data, error } = await userClient
.from('users')
.select('*')
.eq('id', testUserId)
expect(error).toBeNull()
expect(data).toHaveLength(1)
})
it('should prevent users from reading other users data', async () => {
const { data: { session } } = await supabase.auth.signInWithPassword({
email: 'test@example.com',
password: 'test-password-123',
})
const userClient = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{
global: {
headers: {
Authorization: `Bearer ${session!.access_token}`,
},
},
}
)
const { data, error } = await userClient
.from('users')
.select('*')
.neq('id', testUserId)
expect(data).toHaveLength(0) // RLS should filter out other users
})
})
// lib/errors/supabase-errors.ts
import { PostgrestError } from '@supabase/supabase-js'
export class SupabaseError extends Error {
constructor(
public code: string,
public details: string,
public hint?: string
) {
super(details)
this.name = 'SupabaseError'
}
}
export function handleSupabaseError(error: PostgrestError | null): never {
if (!error) {
throw new Error('Unknown error occurred')
}
// RLS Policy Violation
if (error.code === '42501' || error.message.includes('policy')) {
throw new SupabaseError(
'RLS_POLICY_VIOLATION',
'You do not have permission to perform this action',
'Check row-level security policies'
)
}
// Unique Constraint Violation
if (error.code === '23505') {
const field = error.message.match(/Key \((.*?)\)/)?.[1]
throw new SupabaseError(
'DUPLICATE_ENTRY',
`A record with this ${field} already exists`,
'Use a different value or update the existing record'
)
}
// Foreign Key Violation
if (error.code === '23503') {
throw new SupabaseError(
'INVALID_REFERENCE',
'The referenced record does not exist',
'Ensure the related record exists before creating this one'
)
}
// Connection Error
if (error.message.includes('Failed to fetch')) {
throw new SupabaseError(
'CONNECTION_ERROR',
'Unable to connect to the database',
'Check your network connection and Supabase status'
)
}
// Generic Error
throw new SupabaseError(
error.code || 'UNKNOWN_ERROR',
error.message,
error.hint
)
}
// Usage
try {
const { data, error } = await supabase
.from('users')
.insert({ email: 'test@example.com' })
if (error) handleSupabaseError(error)
return data
} catch (err) {
if (err instanceof SupabaseError) {
console.error(`[${err.code}] ${err.details}`)
if (err.hint) console.error(`Hint: ${err.hint}`)
// Return user-friendly error
return {
error: true,
message: err.details,
code: err.code,
}
}
throw err
}
// lib/monitoring/supabase-logger.ts
import { createClient } from '@supabase/supabase-js'
interface QueryLog {
query: string
duration: number
error?: string
timestamp: string
}
export class SupabaseMonitor {
private logs: QueryLog[] = []
constructor(private supabase: ReturnType<typeof createClient>) {
this.wrapClient()
}
private wrapClient() {
const originalFrom = this.supabase.from.bind(this.supabase)
this.supabase.from = (table: string) => {
const startTime = Date.now()
const builder = originalFrom(table)
const wrapMethod = (method: string) => {
const original = (builder as any)[method].bind(builder)
;(builder as any)[method] = async (...args: any[]) => {
const result = await original(...args)
const duration = Date.now() - startTime
this.logs.push({
query: `${method} on ${table}`,
duration,
error: result.error?.message,
timestamp: new Date().toISOString(),
})
// Log slow queries
if (duration > 1000) {
console.warn(`Slow query detected: ${method} on ${table} took ${duration}ms`)
}
return result
}
}
;['select', 'insert', 'update', 'delete', 'upsert'].forEach(wrapMethod)
return builder
}
}
getLogs() {
return this.logs
}
getSlowQueries(threshold = 1000) {
return this.logs.filter(log => log.duration > threshold)
}
getErrorRate() {
const totalQueries = this.logs.length
const errorQueries = this.logs.filter(log => log.error).length
return totalQueries > 0 ? errorQueries / totalQueries : 0
}
}
// scripts/migrate-from-firebase.ts
import admin from 'firebase-admin'
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
admin.initializeApp({
credential: admin.credential.cert('./firebase-credentials.json'),
})
export async function migrateUsers() {
const auth = admin.auth()
let nextPageToken: string | undefined
do {
const listUsersResult = await auth.listUsers(1000, nextPageToken)
for (const userRecord of listUsersResult.users) {
try {
// Create user in Supabase
const { data, error } = await supabase.auth.admin.createUser({
email: userRecord.email!,
email_confirm: true,
user_metadata: {
name: userRecord.displayName,
avatar_url: userRecord.photoURL,
migrated_from_firebase: true,
},
})
if (error) {
console.error(`Failed to migrate user ${userRecord.email}:`, error)
continue
}
console.log(`Migrated user: ${userRecord.email}`)
} catch (err) {
console.error(`Error migrating user ${userRecord.email}:`, err)
}
}
nextPageToken = listUsersResult.pageToken
} while (nextPageToken)
}
export async function migrateFirestoreCollection(
collectionName: string,
tableName: string
) {
const firestore = admin.firestore()
const snapshot = await firestore.collection(collectionName).get()
for (const doc of snapshot.docs) {
const data = doc.data()
// Transform Firestore document to Supabase row
const row = {
id: doc.id,
...data,
// Convert Firestore timestamps
created_at: data.createdAt?._seconds
? new Date(data.createdAt._seconds * 1000).toISOString()
: null,
}
const { error } = await supabase
.from(tableName)
.insert(row)
if (error) {
console.error(`Failed to migrate document ${doc.id}:`, error)
} else {
console.log(`Migrated document: ${doc.id}`)
}
}
}
When providing Supabase guidance, follow this comprehensive format:
# Quick searches by topic
alias sb-auth="grep -r '$1' /Users/zach/Documents/cc-skills/docs/supabase/guides/auth/ -l"
alias sb-db="grep -r '$1' /Users/zach/Documents/cc-skills/docs/supabase/guides/database/ -l"
alias sb-storage="grep -r '$1' /Users/zach/Documents/cc-skills/docs/supabase/guides/storage/ -l"
alias sb-realtime="grep -r '$1' /Users/zach/Documents/cc-skills/docs/supabase/guides/realtime/ -l"
alias sb-functions="grep -r '$1' /Users/zach/Documents/cc-skills/docs/supabase/guides/functions/ -l"
alias sb-ai="grep -r '$1' /Users/zach/Documents/cc-skills/docs/supabase/guides/ai/ -l"
# Find API reference
alias sb-api="grep -r '$1' /Users/zach/Documents/cc-skills/docs/supabase/reference/ -l | head -20"
# Local Development
supabase init # Initialize project
supabase start # Start local Supabase (requires Docker)
supabase status # Check local setup status
supabase stop # Stop local instance
# Database Operations
supabase migration new <name> # Create new migration
supabase db reset # Reset local database
supabase db push # Push migrations to remote
supabase db pull # Pull remote schema
supabase db diff # Show schema differences
# Type Generation
supabase gen types typescript --local # Generate types from local
supabase gen types typescript --project-id <id> # Generate from remote
# Edge Functions
supabase functions new <name> # Create new function
supabase functions serve # Test functions locally
supabase functions deploy <name> # Deploy function
supabase functions logs <name> # View function logs
# Authentication
supabase auth users list # List users
supabase auth users get <user-id> # Get user details
# Secrets Management
supabase secrets set SECRET_NAME=value # Set secret
supabase secrets list # List secrets