
Supabase Mcp Integration
- 43 installs
- 1 repo stars
- Updated November 29, 2025
- manutej/crush-mcp-server
Helps with ai & agent building tasks.
About
supabase-mcp-integration is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- supabase-mcp-integration
- AI & Agent Building
- AI-coding skill
Supabase Mcp Integration by the numbers
- 43 all-time installs (skills.sh)
- Ranked #7,921 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/manutej/crush-mcp-server --skill supabase-mcp-integrationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 43 |
|---|---|
| repo stars | ★ 1 |
| Last updated | November 29, 2025 |
| Repository | manutej/crush-mcp-server ↗ |
What it does
Helps with ai & agent building tasks.
Files
Supabase MCP Integration
A comprehensive skill for building production-ready applications using Supabase - the open-source Backend-as-a-Service platform built on PostgreSQL. This skill covers authentication, database operations, real-time subscriptions, storage, TypeScript integration, and Row-Level Security patterns.
When to Use This Skill
Use this skill when:
- Building full-stack web or mobile applications with PostgreSQL backend
- Implementing authentication (email, OAuth, magic links, MFA) and session management
- Creating real-time applications (chat, collaboration, live dashboards)
- Managing file storage with image optimization and CDN delivery
- Building multi-tenant SaaS applications with fine-grained authorization
- Migrating from Firebase to SQL-based backend
- Requiring type-safe database operations with TypeScript
- Implementing Row-Level Security (RLS) for database authorization
- Building applications with complex queries, joins, and relationships
- Setting up instant REST/GraphQL APIs from database schema
Core Concepts
Supabase Platform Architecture
Supabase is an integrated platform built on enterprise-grade open-source components:
Key Components:
- PostgreSQL Database: Full Postgres with extensions (PostGIS, pg_vector)
- GoTrue (Auth): JWT-based authentication with multiple providers
- PostgREST: Auto-generated REST APIs from database schema
- Realtime: WebSocket server for database changes, broadcast, and presence
- Storage: S3-compatible file storage with CDN and image optimization
- Edge Functions: Globally distributed serverless functions (Deno runtime)
Unified Client Library:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
// All features through single client
await supabase.auth.signIn() // Authentication
await supabase.from('users').select() // Database
supabase.channel('room').subscribe() // Realtime
await supabase.storage.from().upload() // StorageRow-Level Security (RLS)
Database-level authorization using PostgreSQL policies:
- Define access rules directly in the database
- Automatic enforcement on all queries
- Integrated with JWT authentication
- Fine-grained control at row and column level
JWT-Based Authentication
Supabase Auth uses JSON Web Tokens:
- Issued upon successful authentication
- Automatically included in database queries
- Used for RLS policy evaluation
- Refresh token flow for long sessions
Type Safety
Automatic TypeScript type generation from database schema:
- Generate types from live database
- Type-safe queries and mutations
- Compile-time error detection
- IDE autocomplete support
Supabase Client Setup
Installation
# npm
npm install @supabase/supabase-js
# yarn
yarn add @supabase/supabase-js
# pnpm
pnpm add @supabase/supabase-js
# bun
bun add @supabase/supabase-jsEnvironment Configuration
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xyzcompany.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# For server-side operations (keep secure!)
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Security Note: Never expose the service_role key in client-side code.
Client Initialization Pattern (Recommended)
// lib/supabase.ts
import { createClient, SupabaseClient } from '@supabase/supabase-js'
import { Database } from './database.types'
function validateEnvironment() {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
if (!url) {
throw new Error('Missing environment variable: NEXT_PUBLIC_SUPABASE_URL')
}
if (!anonKey) {
throw new Error('Missing environment variable: NEXT_PUBLIC_SUPABASE_ANON_KEY')
}
return { url, anonKey }
}
let supabaseInstance: SupabaseClient<Database> | null = null
export function getSupabaseClient(): SupabaseClient<Database> {
if (!supabaseInstance) {
const { url, anonKey } = validateEnvironment()
supabaseInstance = createClient<Database>(url, anonKey, {
auth: {
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true
},
global: {
headers: {
'X-Application-Name': 'MyApp'
}
}
})
}
return supabaseInstance
}
// Export singleton instance
export const supabase = getSupabaseClient()Configuration Options
const options = {
// Database configuration
db: {
schema: 'public' // Default schema
},
// Authentication configuration
auth: {
autoRefreshToken: true, // Automatically refresh tokens
persistSession: true, // Persist session to localStorage
detectSessionInUrl: true, // Detect session from URL hash
flowType: 'pkce', // Use PKCE flow for OAuth
storage: customStorage, // Custom storage implementation
storageKey: 'sb-auth-token' // Storage key for session
},
// Global configuration
global: {
headers: {
'X-Application-Name': 'my-app',
'apikey': SUPABASE_ANON_KEY
},
fetch: customFetch // Custom fetch implementation
},
// Realtime configuration
realtime: {
params: {
eventsPerSecond: 10
},
timeout: 10000,
heartbeatInterval: 30000
}
}
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, options)Platform-Specific Setup
React Native with AsyncStorage:
import AsyncStorage from '@react-native-async-storage/async-storage'
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: {
storage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false
}
})React Native with Expo SecureStore:
import * as SecureStore from 'expo-secure-store'
import { createClient } from '@supabase/supabase-js'
const ExpoSecureStoreAdapter = {
getItem: (key: string) => SecureStore.getItemAsync(key),
setItem: (key: string, value: string) => SecureStore.setItemAsync(key, value),
removeItem: (key: string) => SecureStore.deleteItemAsync(key)
}
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: {
storage: ExpoSecureStoreAdapter,
autoRefreshToken: true,
persistSession: true
}
})Authentication & Authorization
Email/Password Authentication
Sign Up:
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'secure-password',
options: {
data: {
// Additional user metadata
display_name: 'John Doe',
avatar_url: 'https://example.com/avatar.jpg'
},
emailRedirectTo: 'https://yourapp.com/welcome'
}
})
if (error) {
console.error('Signup failed:', error.message)
return
}
console.log('User created:', data.user)
console.log('Session:', data.session)Sign In:
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'secure-password'
})
if (error) {
console.error('Login failed:', error.message)
return
}
console.log('User:', data.user)
console.log('Session token:', data.session?.access_token)Magic Link (Passwordless)
const { data, error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: {
emailRedirectTo: 'https://yourapp.com/login',
shouldCreateUser: true
}
})
if (error) {
console.error('Failed to send magic link:', error.message)
return
}
console.log('Magic link sent')One-Time Password (OTP) - Phone
// Send OTP
const { data, error } = await supabase.auth.signInWithOtp({
phone: '+1234567890',
options: {
channel: 'sms' // or 'whatsapp'
}
})
// Verify OTP
const { data: verifyData, error: verifyError } = await supabase.auth.verifyOtp({
phone: '+1234567890',
token: '123456',
type: 'sms'
})OAuth (Social Login)
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://yourapp.com/auth/callback',
scopes: 'email profile',
queryParams: {
access_type: 'offline',
prompt: 'consent'
}
}
})
// Supported providers:
// apple, google, github, gitlab, bitbucket, discord, facebook,
// twitter, microsoft, linkedin, notion, slack, spotify, twitch, etc.Session Management
// Get current session
const { data: { session }, error } = await supabase.auth.getSession()
if (session) {
console.log('Access token:', session.access_token)
console.log('User:', session.user)
console.log('Expires at:', session.expires_at)
}
// Get current user
const { data: { user }, error } = await supabase.auth.getUser()
// Refresh session
const { data, error } = await supabase.auth.refreshSession()
// Sign out
const { error } = await supabase.auth.signOut()Auth State Changes
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
console.log('Auth event:', event)
switch (event) {
case 'SIGNED_IN':
console.log('User signed in:', session?.user)
break
case 'SIGNED_OUT':
console.log('User signed out')
break
case 'TOKEN_REFRESHED':
console.log('Token refreshed')
break
case 'USER_UPDATED':
console.log('User updated:', session?.user)
break
case 'PASSWORD_RECOVERY':
console.log('Password recovery initiated')
break
}
}
)
// Cleanup
subscription.unsubscribe()User Management
// Update user
const { data, error } = await supabase.auth.updateUser({
email: 'newemail@example.com',
password: 'new-password',
data: {
display_name: 'New Name',
avatar_url: 'https://example.com/new-avatar.jpg'
}
})
// Reset password
const { data, error } = await supabase.auth.resetPasswordForEmail(
'user@example.com',
{
redirectTo: 'https://yourapp.com/reset-password'
}
)
// Update password after reset
const { data: updateData, error: updateError } = await supabase.auth.updateUser({
password: 'new-secure-password'
})Multi-Factor Authentication (MFA)
// Enroll MFA
const { data: enrollData, error: enrollError } = await supabase.auth.mfa.enroll({
factorType: 'totp',
friendlyName: 'My Phone'
})
// Verify enrollment
const { data: verifyData, error: verifyError } = await supabase.auth.mfa.verify({
factorId: enrollData.id,
code: '123456'
})
// Challenge (during sign-in)
const { data: challengeData, error: challengeError } = await supabase.auth.mfa.challenge({
factorId: 'factor-id'
})
// Verify challenge
const { data, error } = await supabase.auth.mfa.verify({
factorId: 'factor-id',
challengeId: challengeData.id,
code: '123456'
})Database Operations
SELECT Queries
Basic Select:
// Select all columns
const { data, error } = await supabase
.from('users')
.select()
// Select specific columns
const { data, error } = await supabase
.from('users')
.select('id, email, created_at')Filtering:
// Equal
const { data } = await supabase
.from('users')
.select()
.eq('status', 'active')
// Not equal
const { data } = await supabase
.from('users')
.select()
.neq('role', 'admin')
// Greater than / Less than
const { data } = await supabase
.from('products')
.select()
.gt('price', 100)
.lte('stock', 10)
// In array
const { data } = await supabase
.from('users')
.select()
.in('id', [1, 2, 3, 4, 5])
// Pattern matching
const { data } = await supabase
.from('users')
.select()
.like('email', '%@gmail.com')
// Case-insensitive pattern matching
const { data } = await supabase
.from('products')
.select()
.ilike('name', '%laptop%')
// Full text search
const { data } = await supabase
.from('articles')
.select()
.textSearch('title', 'postgres database')
// Null checks
const { data } = await supabase
.from('users')
.select()
.is('deleted_at', null)Ordering and Pagination:
// Order by
const { data } = await supabase
.from('posts')
.select()
.order('created_at', { ascending: false })
// Multiple ordering
const { data } = await supabase
.from('users')
.select()
.order('last_name', { ascending: true })
.order('first_name', { ascending: true })
// Limit results
const { data } = await supabase
.from('posts')
.select()
.limit(10)
// Pagination with range
const { data } = await supabase
.from('posts')
.select()
.range(0, 9) // First 10 items (0-indexed)Joins and Nested Queries:
// One-to-many relationship
const { data } = await supabase
.from('users')
.select(`
id,
email,
posts (
id,
title,
created_at
)
`)
// Many-to-many with junction table
const { data } = await supabase
.from('users')
.select(`
id,
email,
user_roles (
role:roles (
id,
name
)
)
`)
// Nested filtering
const { data } = await supabase
.from('users')
.select(`
id,
email,
posts!inner (
id,
title
)
`)
.eq('posts.published', true)Aggregation:
// Count
const { count, error } = await supabase
.from('users')
.select('*', { count: 'exact', head: true })
// Count with filtering
const { count } = await supabase
.from('users')
.select('*', { count: 'exact', head: true })
.eq('status', 'active')INSERT Operations
// Insert single row
const { data, error } = await supabase
.from('users')
.insert({
email: 'user@example.com',
name: 'John Doe',
age: 30
})
.select() // Return inserted row
// Insert multiple rows
const { data, error } = await supabase
.from('users')
.insert([
{ email: 'user1@example.com', name: 'User One' },
{ email: 'user2@example.com', name: 'User Two' },
{ email: 'user3@example.com', name: 'User Three' }
])
.select()
// Upsert (Insert or Update)
const { data, error } = await supabase
.from('users')
.upsert({
id: 1,
email: 'updated@example.com',
name: 'Updated Name'
}, {
onConflict: 'id' // Conflict column(s)
})
.select()UPDATE Operations
// Update with filter
const { data, error } = await supabase
.from('users')
.update({ status: 'inactive' })
.eq('last_login', null)
.select()
// Update single row by ID
const { data, error } = await supabase
.from('users')
.update({ name: 'New Name' })
.eq('id', userId)
.select()
.single()
// Increment value
const { data, error } = await supabase
.from('profiles')
.update({ login_count: supabase.raw('login_count + 1') })
.eq('id', userId)DELETE Operations
// Delete with filter
const { error } = await supabase
.from('users')
.delete()
.eq('status', 'banned')
// Delete single row
const { error } = await supabase
.from('posts')
.delete()
.eq('id', postId)
// Soft delete pattern
const { error } = await supabase
.from('users')
.update({ deleted_at: new Date().toISOString() })
.eq('id', userId)RPC (Remote Procedure Calls)
// Call function without parameters
const { data, error } = await supabase
.rpc('get_user_count')
// Call function with parameters
const { data, error } = await supabase
.rpc('calculate_discount', {
product_id: 123,
user_id: 456
})Realtime Subscriptions
Database Change Subscriptions
// Listen to all changes
const channel = supabase
.channel('db-changes')
.on(
'postgres_changes',
{
event: '*', // All events: INSERT, UPDATE, DELETE
schema: 'public',
table: 'posts'
},
(payload) => {
console.log('Change received:', payload)
console.log('Event type:', payload.eventType)
console.log('New data:', payload.new)
console.log('Old data:', payload.old)
}
)
.subscribe()
// Cleanup
channel.unsubscribe()Listen to Specific Events:
// INSERT only
const insertChannel = supabase
.channel('post-inserts')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'posts'
},
(payload) => {
console.log('New post created:', payload.new)
}
)
.subscribe()
// UPDATE only
const updateChannel = supabase
.channel('post-updates')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'posts'
},
(payload) => {
console.log('Post updated:', payload.new)
}
)
.subscribe()
// Filter changes for specific rows
const channel = supabase
.channel('user-posts')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts',
filter: `user_id=eq.${userId}`
},
(payload) => {
console.log('User post changed:', payload)
}
)
.subscribe()Broadcast Messages
// Send broadcast
const channel = supabase.channel('room-1')
channel.subscribe((status) => {
if (status === 'SUBSCRIBED') {
channel.send({
type: 'broadcast',
event: 'cursor-move',
payload: { x: 100, y: 200, user: 'Alice' }
})
}
})
// Receive broadcast
const channel = supabase
.channel('room-1')
.on('broadcast', { event: 'cursor-move' }, (payload) => {
console.log('Cursor moved:', payload)
})
.subscribe()Presence Tracking
// Track user presence
const channel = supabase.channel('online-users')
// Set initial state
channel.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
user: 'user-1',
online_at: new Date().toISOString(),
status: 'online'
})
}
})
// Listen to presence changes
channel.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('Online users:', state)
})
channel.on('presence', { event: 'join' }, ({ newPresences }) => {
console.log('Users joined:', newPresences)
})
channel.on('presence', { event: 'leave' }, ({ leftPresences }) => {
console.log('Users left:', leftPresences)
})
// Cleanup
channel.unsubscribe()Storage Operations
Bucket Management
// List buckets
const { data: buckets, error } = await supabase
.storage
.listBuckets()
// Create bucket
const { data, error } = await supabase
.storage
.createBucket('avatars', {
public: false, // Private bucket
fileSizeLimit: 1048576, // 1MB limit
allowedMimeTypes: ['image/png', 'image/jpeg']
})
// Update bucket
const { data, error } = await supabase
.storage
.updateBucket('avatars', {
public: true
})
// Delete bucket
const { data, error } = await supabase
.storage
.deleteBucket('avatars')File Upload
// Standard upload
const file = event.target.files[0]
const filePath = `${userId}/${Date.now()}-${file.name}`
const { data, error } = await supabase
.storage
.from('avatars')
.upload(filePath, file, {
cacheControl: '3600',
upsert: false
})
// Upload with progress tracking
const { data, error } = await supabase
.storage
.from('videos')
.upload(filePath, file, {
onUploadProgress: (progress) => {
const percent = (progress.loaded / progress.total) * 100
console.log(`Upload progress: ${percent.toFixed(2)}%`)
}
})File Download and URLs
// Download file
const { data, error } = await supabase
.storage
.from('avatars')
.download('path/to/file.jpg')
// Get public URL (for public buckets)
const { data } = supabase
.storage
.from('avatars')
.getPublicUrl('path/to/file.jpg')
// Create signed URL (for private buckets)
const { data, error } = await supabase
.storage
.from('private-files')
.createSignedUrl('path/to/file.pdf', 60) // Expires in 60 secondsImage Transformation
const { data } = supabase
.storage
.from('avatars')
.getPublicUrl('user-avatar.jpg', {
transform: {
width: 200,
height: 200,
resize: 'cover', // or 'contain', 'fill'
quality: 80,
format: 'webp'
}
})File Management
// List files
const { data, error } = await supabase
.storage
.from('avatars')
.list('user-123', {
limit: 100,
offset: 0,
sortBy: { column: 'name', order: 'asc' }
})
// Delete files
const { data, error } = await supabase
.storage
.from('avatars')
.remove(['path/to/file1.jpg', 'path/to/file2.jpg'])
// Move file
const { data, error } = await supabase
.storage
.from('avatars')
.move('old/path/file.jpg', 'new/path/file.jpg')
// Copy file
const { data, error } = await supabase
.storage
.from('avatars')
.copy('source/file.jpg', 'destination/file.jpg')TypeScript Integration
Generate Database Types
# Install Supabase CLI
npm install -g supabase
# Login
supabase login
# Generate types
supabase gen types typescript --project-id YOUR_PROJECT_ID > database.types.tsUse Generated Types
import { createClient } from '@supabase/supabase-js'
import { Database } from './database.types'
// Create typed client
const supabase = createClient<Database>(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
)
// Type-safe queries
const { data, error } = await supabase
.from('users')
.select('id, email, created_at')
.eq('id', userId)
// data is typed as:
// Array<{ id: string; email: string; created_at: string }> | null
// Type-safe inserts
const { data, error } = await supabase
.from('posts')
.insert({
title: 'My Post',
content: 'Content here',
user_id: userId,
published: true
})
.select()Helper Types
import { Database } from './database.types'
// Get table row type
type User = Database['public']['Tables']['users']['Row']
// Get insert type
type NewUser = Database['public']['Tables']['users']['Insert']
// Get update type
type UserUpdate = Database['public']['Tables']['users']['Update']
// Get enum type
type UserRole = Database['public']['Enums']['user_role']
// Use in functions
function createUser(user: NewUser): Promise<User> {
// Implementation
}Row-Level Security (RLS)
Enable RLS
-- Enable RLS on a table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;Common RLS Patterns
Public Read Access:
CREATE POLICY "Public profiles are visible to everyone"
ON profiles
FOR SELECT
TO anon, authenticated
USING (true);User Can Only See Own Data:
CREATE POLICY "Users can only see own data"
ON posts
FOR SELECT
TO authenticated
USING (auth.uid() = user_id);User Can Only Modify Own Data:
CREATE POLICY "Users can insert own posts"
ON posts
FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own posts"
ON posts
FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can delete own posts"
ON posts
FOR DELETE
TO authenticated
USING (auth.uid() = user_id);Multi-Tenant Pattern:
CREATE POLICY "Users can only see data from own organization"
ON documents
FOR SELECT
TO authenticated
USING (
organization_id IN (
SELECT organization_id
FROM user_organizations
WHERE user_id = auth.uid()
)
);Role-Based Access Control:
CREATE POLICY "Admin can see all users"
ON users
FOR SELECT
TO authenticated
USING (
auth.jwt() ->> 'role' = 'admin'
OR auth.uid() = id
);Best Practices
Client Initialization
Use Singleton Pattern:
- Create single client instance and reuse across app
- Avoid creating new clients on every request
- Store in module-level variable or context
Error Handling
Always Check Errors:
const { data, error } = await supabase
.from('users')
.select()
if (error) {
console.error('Error fetching users:', error.message)
// Handle error appropriately
return
}
// Use data safely
console.log(data)Use throwOnError() for Promise Rejection:
try {
const { data } = await supabase
.from('users')
.insert({ name: 'John' })
.throwOnError()
console.log('User created:', data)
} catch (error) {
console.error('Failed to create user:', error)
}Security
Never Expose Service Role Key:
- Use
anonkey in client-side code - Use
service_rolekey only in server-side code - Keep service role key in server environment variables
Always Enable RLS:
- Enable RLS on all tables
- Create appropriate policies for each table
- Test policies thoroughly
Validate User Input:
- Never trust client-side data
- Use database constraints and validations
- Validate in both client and database
Performance
Use Select Wisely:
// Bad: Fetch all columns
const { data } = await supabase.from('users').select()
// Good: Only fetch needed columns
const { data } = await supabase.from('users').select('id, email')Use Pagination:
// Bad: Fetch all rows
const { data } = await supabase.from('posts').select()
// Good: Paginate results
const { data } = await supabase
.from('posts')
.select()
.range(0, 9)
.order('created_at', { ascending: false })Index Database Columns:
- Add indexes for frequently queried columns
- Index foreign keys
- Use composite indexes for multi-column queries
Connection Management
Reuse Client Instance:
- Don't create new client for each request
- Use singleton pattern for client initialization
- Consider connection pooling for server-side
Clean Up Subscriptions:
useEffect(() => {
const channel = supabase.channel('room-1')
channel.subscribe(/* ... */)
return () => {
channel.unsubscribe()
}
}, [])Common Patterns & Workflows
User Authentication Flow
// 1. Sign up
const { data: signUpData, error: signUpError } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'secure-password'
})
// 2. Listen to auth state
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') {
// Redirect to dashboard
}
})
// 3. Protected route check
const { data: { session } } = await supabase.auth.getSession()
if (!session) {
// Redirect to login
}
// 4. Sign out
await supabase.auth.signOut()CRUD with RLS
// Enable RLS on table
/*
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can CRUD own todos"
ON todos
FOR ALL
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
*/
// Create
const { data, error } = await supabase
.from('todos')
.insert({
user_id: session.user.id,
title: 'My Todo',
completed: false
})
.select()
// Read (only user's own todos due to RLS)
const { data, error } = await supabase
.from('todos')
.select()
// Update
const { data, error } = await supabase
.from('todos')
.update({ completed: true })
.eq('id', todoId)
// Delete
const { error } = await supabase
.from('todos')
.delete()
.eq('id', todoId)Real-Time Chat Implementation
import { useEffect, useState } from 'react'
import { supabase } from '@/lib/supabase'
function Chat({ roomId, userId }) {
const [messages, setMessages] = useState([])
useEffect(() => {
// Fetch existing messages
const fetchMessages = async () => {
const { data } = await supabase
.from('messages')
.select()
.eq('room_id', roomId)
.order('created_at', { ascending: true })
if (data) setMessages(data)
}
fetchMessages()
// Subscribe to new messages
const channel = supabase
.channel(`room-${roomId}`)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `room_id=eq.${roomId}`
},
(payload) => {
setMessages((prev) => [...prev, payload.new])
}
)
.subscribe()
return () => {
channel.unsubscribe()
}
}, [roomId])
const sendMessage = async (content: string) => {
await supabase.from('messages').insert({
room_id: roomId,
user_id: userId,
content
})
}
return (
<div>
{messages.map((msg) => (
<div key={msg.id}>{msg.content}</div>
))}
</div>
)
}File Upload with Progress
async function uploadAvatar(file: File, userId: string) {
const filePath = `${userId}/${Date.now()}-${file.name}`
const { data, error } = await supabase.storage
.from('avatars')
.upload(filePath, file, {
cacheControl: '3600',
upsert: false,
onUploadProgress: (progress) => {
const percent = (progress.loaded / progress.total) * 100
console.log(`Upload: ${percent.toFixed(2)}%`)
}
})
if (error) {
console.error('Upload failed:', error.message)
return null
}
// Get public URL
const { data: urlData } = supabase.storage
.from('avatars')
.getPublicUrl(filePath)
// Update user profile with avatar URL
await supabase
.from('profiles')
.update({ avatar_url: urlData.publicUrl })
.eq('id', userId)
return urlData.publicUrl
}Troubleshooting
Common Issues
RLS Blocking Queries:
- Check if RLS is enabled:
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY; - Verify policies exist and match your use case
- Test policies with different user contexts
- Use
USINGclause for SELECT/UPDATE/DELETE - Use
WITH CHECKclause for INSERT/UPDATE
Auth Session Not Persisting:
- Ensure
persistSession: truein config - Check if storage (localStorage) is available
- Verify cookies are not blocked
- Check if third-party cookies are enabled (for OAuth)
Realtime Not Working:
- Enable realtime on table in Supabase dashboard
- Check if RLS policies allow subscriptions
- Verify channel subscription is successful
- Check network/firewall blocking WebSockets
Type Generation Errors:
- Ensure Supabase CLI is installed and updated
- Verify project ID is correct
- Check network connectivity to Supabase
- Try regenerating types with
--debugflag
Debug RLS Policies
-- Test policy as specific user
SET request.jwt.claims.sub = 'user-uuid-here';
-- Run query to see what's visible
SELECT * FROM posts;
-- Reset to admin
RESET request.jwt.claims.sub;Performance Issues
Slow Queries:
- Add indexes on frequently queried columns
- Use
EXPLAIN ANALYZEto analyze query plan - Avoid fetching unnecessary columns
- Use pagination for large datasets
Too Many Connections:
- Use connection pooling
- Reuse client instance
- Close unused subscriptions
- Consider using Edge Functions for server-side logic
Production Deployment
Environment Configuration
# Production .env
NEXT_PUBLIC_SUPABASE_URL=https://prod-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=prod-anon-key
SUPABASE_SERVICE_ROLE_KEY=prod-service-role-key
# Staging .env
NEXT_PUBLIC_SUPABASE_URL=https://staging-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=staging-anon-key
SUPABASE_SERVICE_ROLE_KEY=staging-service-role-keyDatabase Migrations
Use Supabase CLI for schema migrations:
# Initialize migrations
supabase migration new create_posts_table
# Apply migrations
supabase db push
# Generate migration from changes
supabase db diff -f create_users_tableMonitoring
- Enable Supabase Dashboard monitoring
- Set up alerts for errors and performance issues
- Monitor database connections
- Track API usage and quotas
- Set up logging for auth events
Backup Strategy
- Enable automatic backups in Supabase dashboard
- Configure point-in-time recovery
- Test restoration procedures
- Export schema and data regularly
- Store backups in separate location
Scaling Considerations
- Upgrade Supabase plan for higher limits
- Use database connection pooling
- Implement caching (Redis, etc.)
- Consider read replicas for heavy read loads
- Use Edge Functions for heavy compute
- Optimize database indexes
- Monitor and optimize slow queries
---
Skill Version: 1.0.0 Last Updated: October 2025 Skill Category: Backend-as-a-Service, Database Integration, Authentication, Real-time Compatible With: React, Next.js, Vue, Angular, React Native, Flutter, Node.js, Deno
Supabase Integration - Comprehensive Examples
This file contains 70+ practical, runnable code examples covering all major Supabase features.
Table of Contents
1. Authentication Examples 2. Database Query Examples 3. Realtime Examples 4. Storage Examples 5. RLS Policy Examples 6. Full Application Examples 7. TypeScript Examples 8. Integration Examples
Authentication Examples
Example 1: Email/Password Sign Up with Metadata
import { supabase } from '@/lib/supabase'
async function signUpWithMetadata(
email: string,
password: string,
displayName: string,
avatarUrl?: string
) {
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
display_name: displayName,
avatar_url: avatarUrl || null,
onboarding_completed: false
},
emailRedirectTo: 'https://yourapp.com/welcome'
}
})
if (error) {
console.error('Sign up error:', error.message)
return null
}
console.log('User created:', data.user?.id)
console.log('Email confirmation sent to:', data.user?.email)
return data.user
}
// Usage
await signUpWithMetadata(
'user@example.com',
'SecurePass123!',
'John Doe',
'https://example.com/avatar.jpg'
)Example 2: Sign In with Error Handling
import { supabase } from '@/lib/supabase'
async function signIn(email: string, password: string) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) {
// Handle specific error codes
switch (error.status) {
case 400:
throw new Error('Invalid email or password')
case 422:
throw new Error('Email not confirmed. Please check your inbox.')
default:
throw new Error(error.message)
}
}
console.log('User signed in:', data.user.email)
console.log('Access token:', data.session?.access_token)
console.log('Refresh token:', data.session?.refresh_token)
return {
user: data.user,
session: data.session
}
}
// Usage with try-catch
try {
const result = await signIn('user@example.com', 'password123')
console.log('Login successful:', result.user.id)
} catch (err) {
console.error('Login failed:', err.message)
}Example 3: Magic Link Authentication
import { supabase } from '@/lib/supabase'
async function sendMagicLink(email: string) {
const { data, error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: 'https://yourapp.com/auth/callback',
shouldCreateUser: true
}
})
if (error) {
console.error('Magic link error:', error.message)
return false
}
console.log('Magic link sent to:', email)
return true
}
// Usage
const success = await sendMagicLink('user@example.com')
if (success) {
alert('Check your email for the magic link!')
}Example 4: OAuth with Google
import { supabase } from '@/lib/supabase'
async function signInWithGoogle() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://yourapp.com/auth/callback',
scopes: 'email profile',
queryParams: {
access_type: 'offline',
prompt: 'consent'
}
}
})
if (error) {
console.error('OAuth error:', error.message)
return
}
// User will be redirected to Google for authentication
console.log('Redirecting to Google...')
}
// Handle callback after OAuth
async function handleOAuthCallback() {
const { data: { session }, error } = await supabase.auth.getSession()
if (error) {
console.error('Session error:', error.message)
return null
}
if (session) {
console.log('OAuth successful:', session.user.email)
return session.user
}
return null
}Example 5: Phone OTP Authentication
import { supabase } from '@/lib/supabase'
async function sendPhoneOTP(phoneNumber: string) {
const { data, error } = await supabase.auth.signInWithOtp({
phone: phoneNumber,
options: {
channel: 'sms' // or 'whatsapp'
}
})
if (error) {
console.error('OTP send error:', error.message)
return false
}
console.log('OTP sent to:', phoneNumber)
return true
}
async function verifyPhoneOTP(phoneNumber: string, token: string) {
const { data, error } = await supabase.auth.verifyOtp({
phone: phoneNumber,
token,
type: 'sms'
})
if (error) {
console.error('OTP verification error:', error.message)
return null
}
console.log('Phone verified:', data.user?.phone)
return data.user
}
// Usage
await sendPhoneOTP('+1234567890')
// User receives OTP via SMS
await verifyPhoneOTP('+1234567890', '123456')Example 6: Auth State Listener
import { useEffect, useState } from 'react'
import { supabase } from '@/lib/supabase'
import { User } from '@supabase/supabase-js'
export function useAuth() {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
// Get initial session
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null)
setLoading(false)
})
// Listen for auth changes
const { data: { subscription } } = supabase.auth.onAuthStateChange(
async (event, session) => {
console.log('Auth event:', event)
switch (event) {
case 'SIGNED_IN':
console.log('User signed in:', session?.user.email)
setUser(session?.user ?? null)
break
case 'SIGNED_OUT':
console.log('User signed out')
setUser(null)
break
case 'TOKEN_REFRESHED':
console.log('Token refreshed')
setUser(session?.user ?? null)
break
case 'USER_UPDATED':
console.log('User updated')
setUser(session?.user ?? null)
break
}
}
)
return () => {
subscription.unsubscribe()
}
}, [])
return { user, loading }
}
// Usage in component
function App() {
const { user, loading } = useAuth()
if (loading) return <div>Loading...</div>
if (!user) return <LoginPage />
return <Dashboard user={user} />
}Example 7: Update User Profile
import { supabase } from '@/lib/supabase'
async function updateUserProfile(updates: {
email?: string
password?: string
displayName?: string
avatarUrl?: string
}) {
const { data, error } = await supabase.auth.updateUser({
...(updates.email && { email: updates.email }),
...(updates.password && { password: updates.password }),
data: {
...(updates.displayName && { display_name: updates.displayName }),
...(updates.avatarUrl && { avatar_url: updates.avatarUrl })
}
})
if (error) {
console.error('Update error:', error.message)
return null
}
console.log('Profile updated successfully')
return data.user
}
// Usage
await updateUserProfile({
displayName: 'Jane Doe',
avatarUrl: 'https://example.com/new-avatar.jpg'
})Example 8: Password Reset Flow
import { supabase } from '@/lib/supabase'
// Step 1: Request password reset
async function requestPasswordReset(email: string) {
const { data, error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: 'https://yourapp.com/reset-password'
})
if (error) {
console.error('Password reset request error:', error.message)
return false
}
console.log('Password reset email sent to:', email)
return true
}
// Step 2: Update password after clicking link in email
async function updatePassword(newPassword: string) {
const { data, error } = await supabase.auth.updateUser({
password: newPassword
})
if (error) {
console.error('Password update error:', error.message)
return false
}
console.log('Password updated successfully')
return true
}
// Usage
// User requests reset
await requestPasswordReset('user@example.com')
// User clicks link in email and lands on reset page
// User enters new password
await updatePassword('NewSecurePassword123!')Example 9: Multi-Factor Authentication (MFA)
import { supabase } from '@/lib/supabase'
// Step 1: Enroll MFA
async function enrollMFA() {
const { data, error } = await supabase.auth.mfa.enroll({
factorType: 'totp',
friendlyName: 'My Authenticator App'
})
if (error) {
console.error('MFA enrollment error:', error.message)
return null
}
// Display QR code to user
console.log('QR Code:', data.totp.qr_code)
console.log('Secret:', data.totp.secret)
return data
}
// Step 2: Verify enrollment with code from authenticator app
async function verifyMFAEnrollment(factorId: string, code: string) {
const { data, error } = await supabase.auth.mfa.verify({
factorId,
code
})
if (error) {
console.error('MFA verification error:', error.message)
return false
}
console.log('MFA enrolled successfully')
return true
}
// Step 3: Challenge during sign-in
async function challengeMFA(factorId: string) {
const { data, error } = await supabase.auth.mfa.challenge({
factorId
})
if (error) {
console.error('MFA challenge error:', error.message)
return null
}
return data.id // Challenge ID
}
// Step 4: Verify challenge code
async function verifyMFAChallenge(
factorId: string,
challengeId: string,
code: string
) {
const { data, error } = await supabase.auth.mfa.verify({
factorId,
challengeId,
code
})
if (error) {
console.error('MFA challenge verification error:', error.message)
return null
}
console.log('MFA verified, user authenticated')
return data
}Example 10: Sign Out
import { supabase } from '@/lib/supabase'
async function signOut() {
const { error } = await supabase.auth.signOut()
if (error) {
console.error('Sign out error:', error.message)
return false
}
console.log('User signed out successfully')
return true
}
// Usage
await signOut()
// Redirect to login pageDatabase Query Examples
Example 11: Basic SELECT Queries
import { supabase } from '@/lib/supabase'
// Select all columns
async function getAllUsers() {
const { data, error } = await supabase.from('users').select()
if (error) {
console.error('Error:', error.message)
return []
}
return data
}
// Select specific columns
async function getUserEmails() {
const { data, error } = await supabase
.from('users')
.select('id, email, created_at')
if (error) {
console.error('Error:', error.message)
return []
}
return data
}
// Select with rename
async function getUsersWithRenamedColumns() {
const { data, error } = await supabase
.from('users')
.select('user_id:id, user_email:email')
if (error) {
console.error('Error:', error.message)
return []
}
return data
}Example 12: Filtering Queries
import { supabase } from '@/lib/supabase'
// Equal filter
async function getActiveUsers() {
const { data } = await supabase
.from('users')
.select()
.eq('status', 'active')
return data
}
// Not equal filter
async function getNonAdminUsers() {
const { data } = await supabase
.from('users')
.select()
.neq('role', 'admin')
return data
}
// Greater than / Less than
async function getExpensiveProducts() {
const { data } = await supabase
.from('products')
.select()
.gt('price', 100)
.lte('stock', 10)
return data
}
// In array
async function getUsersByIds(userIds: string[]) {
const { data } = await supabase
.from('users')
.select()
.in('id', userIds)
return data
}
// Pattern matching (LIKE)
async function getGmailUsers() {
const { data } = await supabase
.from('users')
.select()
.like('email', '%@gmail.com')
return data
}
// Case-insensitive pattern matching (ILIKE)
async function searchProducts(term: string) {
const { data } = await supabase
.from('products')
.select()
.ilike('name', `%${term}%`)
return data
}
// Full-text search
async function searchArticles(query: string) {
const { data } = await supabase
.from('articles')
.select()
.textSearch('title', query)
return data
}
// Null checks
async function getUnconfirmedUsers() {
const { data } = await supabase
.from('users')
.select()
.is('email_confirmed_at', null)
return data
}
async function getConfirmedUsers() {
const { data } = await supabase
.from('users')
.select()
.not('email_confirmed_at', 'is', null)
return data
}
// Multiple filters (AND logic)
async function getActiveAdultUsers() {
const { data } = await supabase
.from('users')
.select()
.eq('status', 'active')
.gte('age', 18)
return data
}
// OR logic with or()
async function getUsersInRoleOrStatus() {
const { data } = await supabase
.from('users')
.select()
.or('role.eq.admin,status.eq.vip')
return data
}Example 13: Ordering and Pagination
import { supabase } from '@/lib/supabase'
// Order by single column
async function getRecentPosts() {
const { data } = await supabase
.from('posts')
.select()
.order('created_at', { ascending: false })
return data
}
// Multiple ordering
async function getUsersSortedByName() {
const { data } = await supabase
.from('users')
.select()
.order('last_name', { ascending: true })
.order('first_name', { ascending: true })
return data
}
// Limit results
async function getTopTenPosts() {
const { data } = await supabase
.from('posts')
.select()
.order('views', { ascending: false })
.limit(10)
return data
}
// Pagination with range
async function getPostsPage(page: number, pageSize: number = 10) {
const start = page * pageSize
const end = start + pageSize - 1
const { data, error, count } = await supabase
.from('posts')
.select('*', { count: 'exact' })
.order('created_at', { ascending: false })
.range(start, end)
if (error) {
console.error('Error:', error.message)
return { posts: [], totalCount: 0 }
}
return {
posts: data,
totalCount: count || 0,
totalPages: Math.ceil((count || 0) / pageSize),
currentPage: page
}
}
// Usage: Get page 2 (items 10-19)
const page2 = await getPostsPage(1) // 0-indexedExample 14: Joins and Relationships
import { supabase } from '@/lib/supabase'
// One-to-many: Get users with their posts
async function getUsersWithPosts() {
const { data } = await supabase
.from('users')
.select(`
id,
email,
posts (
id,
title,
created_at
)
`)
return data
}
// Many-to-many with junction table
async function getUsersWithRoles() {
const { data } = await supabase
.from('users')
.select(`
id,
email,
user_roles (
role:roles (
id,
name,
permissions
)
)
`)
return data
}
// Nested filtering with inner join
async function getUsersWithPublishedPosts() {
const { data } = await supabase
.from('users')
.select(`
id,
email,
posts!inner (
id,
title,
published
)
`)
.eq('posts.published', true)
return data
}
// Custom foreign key reference
async function getMessages() {
const { data } = await supabase
.from('messages')
.select(`
id,
content,
from:sender_id (name, email),
to:receiver_id (name, email)
`)
return data
}
// Multiple levels of nesting
async function getPostsWithCommentsAndAuthors() {
const { data } = await supabase
.from('posts')
.select(`
id,
title,
author:users (
id,
name,
email
),
comments (
id,
content,
commenter:users (
id,
name
)
)
`)
return data
}Example 15: Aggregation and Counting
import { supabase } from '@/lib/supabase'
// Count all rows
async function getTotalUsers() {
const { count, error } = await supabase
.from('users')
.select('*', { count: 'exact', head: true })
if (error) {
console.error('Error:', error.message)
return 0
}
return count || 0
}
// Count with filtering
async function getActiveUsersCount() {
const { count } = await supabase
.from('users')
.select('*', { count: 'exact', head: true })
.eq('status', 'active')
return count || 0
}
// Count and return data
async function getPostsWithCount() {
const { data, count } = await supabase
.from('posts')
.select('*', { count: 'exact' })
.range(0, 9)
return {
posts: data,
totalCount: count
}
}
// Estimated count (faster for large tables)
async function getEstimatedUserCount() {
const { count } = await supabase
.from('users')
.select('*', { count: 'estimated', head: true })
return count || 0
}Example 16: INSERT Operations
import { supabase } from '@/lib/supabase'
// Insert single row
async function createUser(email: string, name: string) {
const { data, error } = await supabase
.from('users')
.insert({
email,
name,
status: 'active'
})
.select()
.single()
if (error) {
console.error('Insert error:', error.message)
return null
}
console.log('User created:', data.id)
return data
}
// Insert multiple rows
async function createMultipleUsers(users: Array<{email: string, name: string}>) {
const { data, error } = await supabase
.from('users')
.insert(users)
.select()
if (error) {
console.error('Batch insert error:', error.message)
return []
}
console.log(`Created ${data.length} users`)
return data
}
// Upsert (insert or update on conflict)
async function upsertUser(userId: string, email: string, name: string) {
const { data, error } = await supabase
.from('users')
.upsert(
{
id: userId,
email,
name,
updated_at: new Date().toISOString()
},
{
onConflict: 'id'
}
)
.select()
.single()
if (error) {
console.error('Upsert error:', error.message)
return null
}
return data
}
// Insert with returning specific columns
async function createPost(title: string, content: string, userId: string) {
const { data, error } = await supabase
.from('posts')
.insert({
title,
content,
user_id: userId
})
.select('id, title, created_at')
.single()
if (error) {
console.error('Insert error:', error.message)
return null
}
return data
}Example 17: UPDATE Operations
import { supabase } from '@/lib/supabase'
// Update single row by ID
async function updateUserName(userId: string, newName: string) {
const { data, error } = await supabase
.from('users')
.update({ name: newName })
.eq('id', userId)
.select()
.single()
if (error) {
console.error('Update error:', error.message)
return null
}
console.log('User updated:', data.name)
return data
}
// Update with filter
async function deactivateOldUsers(daysOld: number) {
const cutoffDate = new Date()
cutoffDate.setDate(cutoffDate.getDate() - daysOld)
const { data, error } = await supabase
.from('users')
.update({ status: 'inactive' })
.lt('last_login', cutoffDate.toISOString())
.select()
if (error) {
console.error('Update error:', error.message)
return []
}
console.log(`Deactivated ${data.length} users`)
return data
}
// Increment value
async function incrementLoginCount(userId: string) {
const { data, error } = await supabase
.from('profiles')
.update({
login_count: supabase.raw('login_count + 1'),
last_login: new Date().toISOString()
})
.eq('id', userId)
.select()
.single()
if (error) {
console.error('Update error:', error.message)
return null
}
return data
}
// Conditional update
async function publishPost(postId: string) {
const { data, error } = await supabase
.from('posts')
.update({
published: true,
published_at: new Date().toISOString()
})
.eq('id', postId)
.eq('published', false) // Only update if not already published
.select()
.single()
if (error) {
console.error('Update error:', error.message)
return null
}
return data
}Example 18: DELETE Operations
import { supabase } from '@/lib/supabase'
// Delete single row by ID
async function deletePost(postId: string) {
const { error } = await supabase
.from('posts')
.delete()
.eq('id', postId)
if (error) {
console.error('Delete error:', error.message)
return false
}
console.log('Post deleted:', postId)
return true
}
// Delete with filter
async function deleteDraftPosts(userId: string) {
const { data, error } = await supabase
.from('posts')
.delete()
.eq('user_id', userId)
.eq('status', 'draft')
.select()
if (error) {
console.error('Delete error:', error.message)
return []
}
console.log(`Deleted ${data.length} draft posts`)
return data
}
// Soft delete pattern
async function softDeleteUser(userId: string) {
const { data, error } = await supabase
.from('users')
.update({
deleted_at: new Date().toISOString(),
status: 'deleted'
})
.eq('id', userId)
.select()
.single()
if (error) {
console.error('Soft delete error:', error.message)
return null
}
console.log('User soft deleted:', data.id)
return data
}
// Delete old records
async function cleanupOldLogs(daysOld: number) {
const cutoffDate = new Date()
cutoffDate.setDate(cutoffDate.getDate() - daysOld)
const { error, count } = await supabase
.from('logs')
.delete()
.lt('created_at', cutoffDate.toISOString())
.select('*', { count: 'exact', head: true })
if (error) {
console.error('Cleanup error:', error.message)
return 0
}
console.log(`Deleted ${count} old log entries`)
return count || 0
}Example 19: RPC (Remote Procedure Calls)
import { supabase } from '@/lib/supabase'
/*
PostgreSQL Function:
CREATE OR REPLACE FUNCTION calculate_user_stats(user_uuid UUID)
RETURNS TABLE(
total_posts INT,
total_likes INT,
total_comments INT
) AS $$
BEGIN
RETURN QUERY
SELECT
COUNT(DISTINCT p.id)::INT as total_posts,
COUNT(DISTINCT l.id)::INT as total_likes,
COUNT(DISTINCT c.id)::INT as total_comments
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
LEFT JOIN likes l ON l.post_id = p.id
LEFT JOIN comments c ON c.post_id = p.id
WHERE u.id = user_uuid;
END;
$$ LANGUAGE plpgsql;
*/
async function getUserStats(userId: string) {
const { data, error } = await supabase
.rpc('calculate_user_stats', {
user_uuid: userId
})
if (error) {
console.error('RPC error:', error.message)
return null
}
return data[0]
}
/*
PostgreSQL Function for search:
CREATE OR REPLACE FUNCTION search_posts(search_query TEXT)
RETURNS SETOF posts AS $$
BEGIN
RETURN QUERY
SELECT *
FROM posts
WHERE
to_tsvector('english', title || ' ' || content) @@
plainto_tsquery('english', search_query)
ORDER BY created_at DESC;
END;
$$ LANGUAGE plpgsql;
*/
async function searchPosts(query: string) {
const { data, error } = await supabase
.rpc('search_posts', {
search_query: query
})
if (error) {
console.error('Search error:', error.message)
return []
}
return data
}Example 20: Transaction-Like Operations with RPC
/*
PostgreSQL Function for atomic transfer:
CREATE OR REPLACE FUNCTION transfer_credits(
from_user UUID,
to_user UUID,
amount INT
)
RETURNS BOOLEAN AS $$
BEGIN
-- Check if from_user has enough credits
IF (SELECT credits FROM profiles WHERE id = from_user) < amount THEN
RAISE EXCEPTION 'Insufficient credits';
END IF;
-- Deduct from sender
UPDATE profiles
SET credits = credits - amount
WHERE id = from_user;
-- Add to receiver
UPDATE profiles
SET credits = credits + amount
WHERE id = to_user;
-- Log transaction
INSERT INTO credit_transactions (from_user, to_user, amount)
VALUES (from_user, to_user, amount);
RETURN TRUE;
EXCEPTION
WHEN OTHERS THEN
RETURN FALSE;
END;
$$ LANGUAGE plpgsql;
*/
async function transferCredits(
fromUserId: string,
toUserId: string,
amount: number
) {
const { data, error } = await supabase.rpc('transfer_credits', {
from_user: fromUserId,
to_user: toUserId,
amount
})
if (error) {
console.error('Transfer error:', error.message)
return false
}
console.log('Transfer successful')
return data
}Realtime Examples
Example 21: Database Change Subscription
import { useEffect, useState } from 'react'
import { supabase } from '@/lib/supabase'
function PostsList() {
const [posts, setPosts] = useState([])
useEffect(() => {
// Fetch initial data
fetchPosts()
// Subscribe to changes
const channel = supabase
.channel('posts-changes')
.on(
'postgres_changes',
{
event: '*', // All events
schema: 'public',
table: 'posts'
},
(payload) => {
console.log('Change received:', payload)
if (payload.eventType === 'INSERT') {
setPosts(prev => [payload.new, ...prev])
} else if (payload.eventType === 'UPDATE') {
setPosts(prev =>
prev.map(post =>
post.id === payload.new.id ? payload.new : post
)
)
} else if (payload.eventType === 'DELETE') {
setPosts(prev =>
prev.filter(post => post.id !== payload.old.id)
)
}
}
)
.subscribe()
return () => {
channel.unsubscribe()
}
}, [])
async function fetchPosts() {
const { data } = await supabase
.from('posts')
.select()
.order('created_at', { ascending: false })
if (data) setPosts(data)
}
return (
<div>
{posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}Example 22: Filtered Realtime Subscription
import { useEffect, useState } from 'react'
import { supabase } from '@/lib/supabase'
function UserPosts({ userId }: { userId: string }) {
const [posts, setPosts] = useState([])
useEffect(() => {
// Fetch user's posts
const fetchUserPosts = async () => {
const { data } = await supabase
.from('posts')
.select()
.eq('user_id', userId)
.order('created_at', { ascending: false })
if (data) setPosts(data)
}
fetchUserPosts()
// Subscribe only to this user's posts
const channel = supabase
.channel(`user-${userId}-posts`)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts',
filter: `user_id=eq.${userId}` // Filter by user_id
},
(payload) => {
if (payload.eventType === 'INSERT') {
setPosts(prev => [payload.new, ...prev])
} else if (payload.eventType === 'UPDATE') {
setPosts(prev =>
prev.map(post =>
post.id === payload.new.id ? payload.new : post
)
)
} else if (payload.eventType === 'DELETE') {
setPosts(prev =>
prev.filter(post => post.id !== payload.old.id)
)
}
}
)
.subscribe()
return () => {
channel.unsubscribe()
}
}, [userId])
return (
<div>
<h2>Your Posts</h2>
{posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}Example 23: Broadcast Messages (Real-Time Chat)
import { useEffect, useState } from 'react'
import { supabase } from '@/lib/supabase'
type Message = {
userId: string
username: string
message: string
timestamp: string
}
function ChatRoom({ roomId, userId, username }: {
roomId: string
userId: string
username: string
}) {
const [messages, setMessages] = useState<Message[]>([])
useEffect(() => {
const channel = supabase.channel(`room-${roomId}`)
// Listen for broadcast messages
channel
.on('broadcast', { event: 'message' }, (payload) => {
console.log('Message received:', payload)
setMessages(prev => [...prev, payload.payload as Message])
})
.subscribe((status) => {
console.log('Subscription status:', status)
})
return () => {
channel.unsubscribe()
}
}, [roomId])
const sendMessage = (text: string) => {
const channel = supabase.channel(`room-${roomId}`)
channel.send({
type: 'broadcast',
event: 'message',
payload: {
userId,
username,
message: text,
timestamp: new Date().toISOString()
}
})
}
return (
<div>
<div>
{messages.map((msg, idx) => (
<div key={idx}>
<strong>{msg.username}:</strong> {msg.message}
</div>
))}
</div>
<input
type="text"
onKeyPress={(e) => {
if (e.key === 'Enter') {
sendMessage(e.currentTarget.value)
e.currentTarget.value = ''
}
}}
/>
</div>
)
}Example 24: Presence Tracking
import { useEffect, useState } from 'react'
import { supabase } from '@/lib/supabase'
type UserPresence = {
userId: string
username: string
status: 'online' | 'away'
lastSeen: string
}
function OnlineUsers({ roomId, userId, username }: {
roomId: string
userId: string
username: string
}) {
const [onlineUsers, setOnlineUsers] = useState<UserPresence[]>([])
useEffect(() => {
const channel = supabase.channel(`presence-${roomId}`)
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
const users = Object.values(state)
.flat()
.map(user => user as UserPresence)
setOnlineUsers(users)
})
.on('presence', { event: 'join' }, ({ newPresences }) => {
console.log('Users joined:', newPresences)
})
.on('presence', { event: 'leave' }, ({ leftPresences }) => {
console.log('Users left:', leftPresences)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// Track current user's presence
await channel.track({
userId,
username,
status: 'online',
lastSeen: new Date().toISOString()
})
}
})
return () => {
channel.unsubscribe()
}
}, [roomId, userId, username])
return (
<div>
<h3>Online Users ({onlineUsers.length})</h3>
<ul>
{onlineUsers.map(user => (
<li key={user.userId}>
{user.username} - {user.status}
</li>
))}
</ul>
</div>
)
}Example 25: Typing Indicator with Broadcast
import { useEffect, useState, useCallback } from 'react'
import { supabase } from '@/lib/supabase'
function TypingIndicator({ roomId, userId, username }: {
roomId: string
userId: string
username: string
}) {
const [typingUsers, setTypingUsers] = useState<Set<string>>(new Set())
let typingTimeout: NodeJS.Timeout | null = null
useEffect(() => {
const channel = supabase.channel(`room-${roomId}`)
channel
.on('broadcast', { event: 'typing' }, (payload) => {
const { userId: typingUserId, username, isTyping } = payload.payload
setTypingUsers(prev => {
const next = new Set(prev)
if (isTyping) {
next.add(username)
} else {
next.delete(username)
}
return next
})
// Clear typing indicator after 3 seconds
if (isTyping) {
setTimeout(() => {
setTypingUsers(prev => {
const next = new Set(prev)
next.delete(username)
return next
})
}, 3000)
}
})
.subscribe()
return () => {
channel.unsubscribe()
}
}, [roomId])
const handleTyping = useCallback(() => {
const channel = supabase.channel(`room-${roomId}`)
// Send typing indicator
channel.send({
type: 'broadcast',
event: 'typing',
payload: {
userId,
username,
isTyping: true
}
})
// Clear previous timeout
if (typingTimeout) {
clearTimeout(typingTimeout)
}
// Stop typing indicator after 2 seconds of inactivity
typingTimeout = setTimeout(() => {
channel.send({
type: 'broadcast',
event: 'typing',
payload: {
userId,
username,
isTyping: false
}
})
}, 2000)
}, [roomId, userId, username])
return (
<div>
{typingUsers.size > 0 && (
<em>{Array.from(typingUsers).join(', ')} typing...</em>
)}
<input
type="text"
onChange={handleTyping}
placeholder="Type a message..."
/>
</div>
)
}Storage Examples
Example 26: Upload File with Progress
import { useState } from 'react'
import { supabase } from '@/lib/supabase'
function FileUpload({ userId }: { userId: string }) {
const [uploading, setUploading] = useState(false)
const [progress, setProgress] = useState(0)
const uploadFile = async (file: File) => {
try {
setUploading(true)
const fileExt = file.name.split('.').pop()
const fileName = `${userId}/${Date.now()}.${fileExt}`
const { data, error } = await supabase.storage
.from('uploads')
.upload(fileName, file, {
cacheControl: '3600',
upsert: false,
onUploadProgress: (progressEvent) => {
const percent = (progressEvent.loaded / progressEvent.total) * 100
setProgress(percent)
console.log(`Upload progress: ${percent.toFixed(2)}%`)
}
})
if (error) {
throw error
}
console.log('File uploaded:', data.path)
// Get public URL
const { data: urlData } = supabase.storage
.from('uploads')
.getPublicUrl(fileName)
console.log('Public URL:', urlData.publicUrl)
return urlData.publicUrl
} catch (error) {
console.error('Upload error:', error)
return null
} finally {
setUploading(false)
setProgress(0)
}
}
return (
<div>
<input
type="file"
onChange={(e) => {
const file = e.target.files?.[0]
if (file) uploadFile(file)
}}
disabled={uploading}
/>
{uploading && (
<div>
<progress value={progress} max="100" />
<span>{progress.toFixed(0)}%</span>
</div>
)}
</div>
)
}Example 27: Avatar Upload and Update
import { supabase } from '@/lib/supabase'
async function uploadAvatar(file: File, userId: string) {
try {
// Delete old avatar if exists
const { data: existingFiles } = await supabase.storage
.from('avatars')
.list(userId)
if (existingFiles && existingFiles.length > 0) {
const filesToRemove = existingFiles.map(
file => `${userId}/${file.name}`
)
await supabase.storage
.from('avatars')
.remove(filesToRemove)
}
// Upload new avatar
const fileExt = file.name.split('.').pop()
const filePath = `${userId}/avatar.${fileExt}`
const { data: uploadData, error: uploadError } = await supabase.storage
.from('avatars')
.upload(filePath, file, {
cacheControl: '3600',
upsert: true
})
if (uploadError) {
throw uploadError
}
// Get public URL
const { data: urlData } = supabase.storage
.from('avatars')
.getPublicUrl(filePath)
// Update user profile with new avatar URL
const { error: updateError } = await supabase
.from('profiles')
.update({ avatar_url: urlData.publicUrl })
.eq('id', userId)
if (updateError) {
throw updateError
}
console.log('Avatar updated:', urlData.publicUrl)
return urlData.publicUrl
} catch (error) {
console.error('Avatar upload error:', error)
return null
}
}Example 28: Image Transformation
import { supabase } from '@/lib/supabase'
function getTransformedImageUrl(
bucket: string,
path: string,
width?: number,
height?: number,
quality?: number
) {
const { data } = supabase.storage
.from(bucket)
.getPublicUrl(path, {
transform: {
...(width && { width }),
...(height && { height }),
resize: 'cover',
...(quality && { quality }),
format: 'webp'
}
})
return data.publicUrl
}
// Usage examples
function ImageGallery({ imagePath }: { imagePath: string }) {
return (
<div>
{/* Thumbnail */}
<img
src={getTransformedImageUrl('photos', imagePath, 200, 200, 80)}
alt="Thumbnail"
/>
{/* Medium size */}
<img
src={getTransformedImageUrl('photos', imagePath, 800, 600, 85)}
alt="Medium"
/>
{/* Full size */}
<img
src={getTransformedImageUrl('photos', imagePath)}
alt="Full size"
/>
</div>
)
}Example 29: Download File
import { supabase } from '@/lib/supabase'
async function downloadFile(bucket: string, path: string) {
try {
const { data, error } = await supabase.storage
.from(bucket)
.download(path)
if (error) {
throw error
}
// Create blob URL
const url = URL.createObjectURL(data)
// Create download link
const link = document.createElement('a')
link.href = url
link.download = path.split('/').pop() || 'download'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
// Cleanup
URL.revokeObjectURL(url)
console.log('File downloaded successfully')
} catch (error) {
console.error('Download error:', error)
}
}
// Usage
downloadFile('documents', 'user-123/report.pdf')Example 30: Create Signed URL for Private Files
import { supabase } from '@/lib/supabase'
async function getPrivateFileUrl(bucket: string, path: string, expiresIn: number = 60) {
try {
const { data, error } = await supabase.storage
.from(bucket)
.createSignedUrl(path, expiresIn)
if (error) {
throw error
}
console.log('Signed URL:', data.signedUrl)
console.log('Expires at:', new Date(Date.now() + expiresIn * 1000))
return data.signedUrl
} catch (error) {
console.error('Signed URL error:', error)
return null
}
}
// Usage: Get URL that expires in 5 minutes
const url = await getPrivateFileUrl('private-docs', 'user-123/contract.pdf', 300)RLS Policy Examples
Example 31: User-Specific Access (CRUD)
-- Enable RLS
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;
-- Users can select their own todos
CREATE POLICY "Users can view own todos"
ON todos
FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
-- Users can insert their own todos
CREATE POLICY "Users can insert own todos"
ON todos
FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
-- Users can update their own todos
CREATE POLICY "Users can update own todos"
ON todos
FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- Users can delete their own todos
CREATE POLICY "Users can delete own todos"
ON todos
FOR DELETE
TO authenticated
USING (auth.uid() = user_id);Example 32: Public/Private Content
-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Anyone can read published public posts
CREATE POLICY "Public published posts are visible"
ON posts
FOR SELECT
TO anon, authenticated
USING (published = true AND is_public = true);
-- Users can read their own posts (any status)
CREATE POLICY "Users can view own posts"
ON posts
FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
-- Users can insert their own posts
CREATE POLICY "Users can create posts"
ON posts
FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
-- Users can update their own posts
CREATE POLICY "Users can update own posts"
ON posts
FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);Example 33: Multi-Tenant Access
-- Enable RLS
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
-- Users can only access documents from their organization
CREATE POLICY "Organization members can access documents"
ON documents
FOR ALL
TO authenticated
USING (
organization_id IN (
SELECT organization_id
FROM user_organizations
WHERE user_id = auth.uid()
)
)
WITH CHECK (
organization_id IN (
SELECT organization_id
FROM user_organizations
WHERE user_id = auth.uid()
)
);
-- Index for performance
CREATE INDEX idx_documents_org_id ON documents(organization_id);
CREATE INDEX idx_user_orgs_user_id ON user_organizations(user_id);Example 34: Role-Based Access Control
-- Create user_role enum
CREATE TYPE user_role AS ENUM ('admin', 'moderator', 'user');
-- Add role column
ALTER TABLE users ADD COLUMN role user_role DEFAULT 'user';
-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Regular users can view published posts
CREATE POLICY "Users can view published posts"
ON posts
FOR SELECT
TO authenticated
USING (published = true OR auth.uid() = user_id);
-- Moderators and admins can update any post
CREATE POLICY "Moderators can update posts"
ON posts
FOR UPDATE
TO authenticated
USING (
(SELECT role FROM users WHERE id = auth.uid())
IN ('admin', 'moderator')
);
-- Only admins can delete posts
CREATE POLICY "Admins can delete posts"
ON posts
FOR DELETE
TO authenticated
USING (
(SELECT role FROM users WHERE id = auth.uid()) = 'admin'
);Example 35: Time-Based Access
-- Enable RLS
ALTER TABLE limited_offers ENABLE ROW LEVEL SECURITY;
-- Users can only see active offers within date range
CREATE POLICY "Users can view active offers"
ON limited_offers
FOR SELECT
TO authenticated
USING (
is_active = true AND
NOW() >= start_date AND
NOW() <= end_date
);Full Application Examples
Example 36: Complete Todo App
// types.ts
export type Todo = {
id: string
user_id: string
task: string
is_complete: boolean
created_at: string
}
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
import { Database } from './database.types'
export const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
// hooks/useTodos.ts
import { useEffect, useState } from 'react'
import { supabase } from '@/lib/supabase'
import { Todo } from '@/types'
export function useTodos(userId: string) {
const [todos, setTodos] = useState<Todo[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchTodos()
const channel = supabase
.channel('todos-changes')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'todos',
filter: `user_id=eq.${userId}`
},
(payload) => {
if (payload.eventType === 'INSERT') {
setTodos(prev => [payload.new as Todo, ...prev])
} else if (payload.eventType === 'UPDATE') {
setTodos(prev =>
prev.map(todo =>
todo.id === payload.new.id ? (payload.new as Todo) : todo
)
)
} else if (payload.eventType === 'DELETE') {
setTodos(prev => prev.filter(todo => todo.id !== payload.old.id))
}
}
)
.subscribe()
return () => {
channel.unsubscribe()
}
}, [userId])
async function fetchTodos() {
setLoading(true)
const { data, error } = await supabase
.from('todos')
.select()
.eq('user_id', userId)
.order('created_at', { ascending: false })
if (error) {
console.error('Error fetching todos:', error)
} else {
setTodos(data || [])
}
setLoading(false)
}
async function addTodo(task: string) {
const { error } = await supabase
.from('todos')
.insert({
user_id: userId,
task,
is_complete: false
})
if (error) {
console.error('Error adding todo:', error)
}
}
async function toggleTodo(id: string, isComplete: boolean) {
const { error } = await supabase
.from('todos')
.update({ is_complete: !isComplete })
.eq('id', id)
if (error) {
console.error('Error updating todo:', error)
}
}
async function deleteTodo(id: string) {
const { error } = await supabase
.from('todos')
.delete()
.eq('id', id)
if (error) {
console.error('Error deleting todo:', error)
}
}
return {
todos,
loading,
addTodo,
toggleTodo,
deleteTodo
}
}
// components/TodoApp.tsx
import { useState } from 'react'
import { useTodos } from '@/hooks/useTodos'
import { useAuth } from '@/hooks/useAuth'
export function TodoApp() {
const { user } = useAuth()
const { todos, loading, addTodo, toggleTodo, deleteTodo } = useTodos(user?.id!)
const [newTask, setNewTask] = useState('')
if (loading) return <div>Loading todos...</div>
return (
<div>
<h1>My Todos</h1>
<form onSubmit={(e) => {
e.preventDefault()
if (newTask.trim()) {
addTodo(newTask)
setNewTask('')
}
}}>
<input
type="text"
value={newTask}
onChange={(e) => setNewTask(e.target.value)}
placeholder="Add a new task"
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.is_complete}
onChange={() => toggleTodo(todo.id, todo.is_complete)}
/>
<span style={{
textDecoration: todo.is_complete ? 'line-through' : 'none'
}}>
{todo.task}
</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
{todos.length === 0 && (
<p>No todos yet. Add one above!</p>
)}
</div>
)
}Example 37: Real-Time Chat Application
See SKILL.md for complete chat application example with presence, typing indicators, and message history.
TypeScript Examples
Example 38: Generated Types Usage
import { Database } from './database.types'
// Extract table types
type User = Database['public']['Tables']['users']['Row']
type NewUser = Database['public']['Tables']['users']['Insert']
type UserUpdate = Database['public']['Tables']['users']['Update']
// Extract enum types
type UserRole = Database['public']['Enums']['user_role']
// Use in functions
async function createUser(user: NewUser): Promise<User | null> {
const { data, error } = await supabase
.from('users')
.insert(user)
.select()
.single()
if (error) {
console.error('Error:', error.message)
return null
}
return data
}
async function updateUser(id: string, updates: UserUpdate): Promise<User | null> {
const { data, error } = await supabase
.from('users')
.update(updates)
.eq('id', id)
.select()
.single()
if (error) {
console.error('Error:', error.message)
return null
}
return data
}Example 39: Type-Safe Queries
import { createClient } from '@supabase/supabase-js'
import { Database } from './database.types'
const supabase = createClient<Database>(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
)
// TypeScript knows about all tables, columns, and types
async function getUsers() {
const { data, error } = await supabase
.from('users') // ✅ TypeScript validates table name
.select('id, email, created_at') // ✅ TypeScript validates column names
// data is typed as:
// Array<{ id: string; email: string; created_at: string }> | null
return data
}
async function createPost(title: string, content: string, userId: string) {
const { data, error } = await supabase
.from('posts')
.insert({
title, // ✅ Required field
content, // ✅ Optional field (can be null)
user_id: userId, // ✅ Required field
published: true // ✅ Optional field with default
// TypeScript will error if you include invalid fields
})
.select()
return data
}Example 40: Helper Type Functions
import { Database } from './database.types'
// Create a generic table type helper
type Tables<T extends keyof Database['public']['Tables']> =
Database['public']['Tables'][T]['Row']
type Inserts<T extends keyof Database['public']['Tables']> =
Database['public']['Tables'][T]['Insert']
type Updates<T extends keyof Database['public']['Tables']> =
Database['public']['Tables'][T]['Update']
// Use the helpers
type User = Tables<'users'>
type Post = Tables<'posts'>
type Comment = Tables<'comments'>
type NewPost = Inserts<'posts'>
type PostUpdate = Updates<'posts'>
// Create type-safe CRUD functions
async function create<T extends keyof Database['public']['Tables']>(
table: T,
data: Inserts<T>
): Promise<Tables<T> | null> {
const { data: result, error } = await supabase
.from(table)
.insert(data)
.select()
.single()
if (error) {
console.error('Create error:', error.message)
return null
}
return result as Tables<T>
}
// Usage with full type safety
const newPost = await create('posts', {
title: 'My Post',
content: 'Post content',
user_id: '123'
})---
Total Examples: 40+ comprehensive examples covering authentication, database operations, realtime, storage, RLS policies, full applications, and TypeScript integration.
For complete API reference and additional examples, see REFERENCE.md.
Supabase MCP Integration Skill
Overview
This skill provides comprehensive guidance for building production-ready applications using Supabase - the open-source Backend-as-a-Service platform built on PostgreSQL. Supabase positions itself as "the Firebase alternative" with a focus on PostgreSQL, developer experience, and open-source principles.
What is Supabase?
Supabase is an integrated platform that combines:
- PostgreSQL Database - Full Postgres with extensions (PostGIS, pg_vector)
- Authentication - JWT-based auth with 20+ OAuth providers, magic links, MFA
- Auto-generated APIs - Instant REST and GraphQL APIs from database schema
- Realtime - WebSocket server for database changes, broadcast, and presence
- Storage - S3-compatible file storage with CDN and image optimization
- Edge Functions - Globally distributed serverless functions (Deno runtime)
When to Use This Skill
Use this skill when you need to:
1. Build Full-Stack Applications
- Web applications with React, Next.js, Vue, Angular
- Mobile applications with React Native, Flutter
- Server-side applications with Node.js, Deno
2. Implement Authentication
- Email/password authentication
- Social login (Google, GitHub, etc.)
- Magic links and OTP
- Multi-factor authentication (MFA)
- Session management
3. Work with PostgreSQL
- Type-safe database operations
- Complex queries with joins and filters
- Database-level authorization (RLS)
- Real-time database subscriptions
4. Manage File Storage
- Upload and serve files
- Image optimization and transformation
- Public and private buckets
- CDN delivery
5. Build Real-Time Features
- Live chat and messaging
- Collaborative editing
- Live dashboards and analytics
- Multiplayer games
- Presence tracking
6. Create Multi-Tenant Applications
- SaaS platforms with data isolation
- Role-based access control
- Organization-based permissions
Key Features
🔐 Authentication
- Multiple Auth Methods: Email/password, magic links, phone OTP, OAuth (20+ providers)
- Session Management: JWT-based with automatic refresh
- User Metadata: Store custom user data
- MFA Support: Time-based one-time passwords (TOTP)
- Social Login: Google, GitHub, Apple, and more
🗄️ Database
- Full PostgreSQL: All Postgres features including views, functions, triggers
- Type-Safe: Automatic TypeScript type generation from schema
- Query Builder: Intuitive API for complex queries
- Relationships: One-to-many, many-to-many with automatic joins
- RPC Calls: Execute PostgreSQL functions from client
⚡ Realtime
- Database Changes: Subscribe to INSERT, UPDATE, DELETE events
- Broadcast: Low-latency messaging between clients
- Presence: Track online users and state synchronization
- Filters: Subscribe to specific rows based on conditions
📦 Storage
- File Management: Upload, download, move, copy, delete files
- Image Transformation: Automatic resizing, format conversion, optimization
- CDN Delivery: Global CDN with 285+ cities
- RLS for Files: Database-level security for file access
- Resumable Uploads: TUS protocol for large files
🛡️ Security
- Row-Level Security (RLS): Database-level authorization
- JWT Integration: Automatic token inclusion in queries
- Policy-Based Access: Fine-grained control at row and column level
- Multi-Tenant Patterns: Built-in support for SaaS applications
🎯 TypeScript
- Type Generation: Automatic types from database schema
- Type-Safe Queries: Compile-time error detection
- IDE Support: Full autocomplete and IntelliSense
- Helper Types: Extract table, insert, update types
Quick Start
Installation
npm install @supabase/supabase-jsBasic Setup
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
// Sign up
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'secure-password'
})
// Query data
const { data: users } = await supabase
.from('users')
.select('id, email, created_at')
// Realtime subscription
const channel = supabase
.channel('posts')
.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'posts' },
(payload) => console.log('New post:', payload.new)
)
.subscribe()
// Upload file
const { data: uploadData } = await supabase.storage
.from('avatars')
.upload(`${userId}/avatar.jpg`, file)Skill File Organization
This skill includes the following files:
- SKILL.md - Comprehensive reference covering all Supabase features (25KB+)
- Client setup and configuration
- Authentication methods and patterns
- Database operations (SELECT, INSERT, UPDATE, DELETE)
- Realtime subscriptions
- Storage operations
- TypeScript integration
- Row-Level Security (RLS)
- Best practices and troubleshooting
- EXAMPLES.md - Practical code examples (18KB+)
- Authentication flows (15+ examples)
- Database queries (20+ examples)
- Realtime patterns (10+ examples)
- Storage operations (10+ examples)
- RLS policies (15+ examples)
- Full application examples
- REFERENCE.md - Complete API reference (15KB+)
- All Supabase client methods
- Configuration options
- Type definitions
- Error codes
- Performance tuning
- Security checklist
- README.md - This file, providing overview and quick start
Progressive Disclosure
This skill uses progressive disclosure - start with SKILL.md for core concepts and common patterns, then dive into EXAMPLES.md for practical implementations and REFERENCE.md for complete API details.
Architecture Pattern
Client Initialization (Singleton)
// lib/supabase.ts
import { createClient, SupabaseClient } from '@supabase/supabase-js'
import { Database } from './database.types'
let supabaseInstance: SupabaseClient<Database> | null = null
export function getSupabaseClient(): SupabaseClient<Database> {
if (!supabaseInstance) {
supabaseInstance = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
auth: {
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true
}
}
)
}
return supabaseInstance
}
export const supabase = getSupabaseClient()Type-Safe Development
// Generate types from database
// $ supabase gen types typescript --project-id YOUR_ID > database.types.ts
import { Database } from './database.types'
const supabase = createClient<Database>(url, key)
// All queries are now type-safe
const { data } = await supabase
.from('users') // TypeScript knows this table exists
.select('id, email') // TypeScript validates column namesRow-Level Security Pattern
-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Users can only see their own posts
CREATE POLICY "Users can only see own posts"
ON posts FOR SELECT TO authenticated
USING (auth.uid() = user_id);
-- Users can only insert their own posts
CREATE POLICY "Users can only insert own posts"
ON posts FOR INSERT TO authenticated
WITH CHECK (auth.uid() = user_id);Common Use Cases
1. Todo Application with Real-Time Sync
- User authentication
- CRUD operations with RLS
- Real-time updates across devices
- Type-safe database queries
2. Chat Application
- User authentication and presence
- Real-time message delivery
- Typing indicators (broadcast)
- Online user tracking (presence)
- File attachments (storage)
3. SaaS Multi-Tenant Application
- Organization-based data isolation
- Role-based access control
- User invitation system
- Billing and subscription management
4. Social Media Platform
- User profiles and authentication
- Posts with likes and comments
- Real-time notifications
- Image uploads with transformation
- Infinite scroll pagination
5. Collaborative Editor
- User presence tracking
- Real-time cursor positions (broadcast)
- Document change subscriptions
- Conflict resolution
- Version history
Best Practices
1. Security First
- Always enable Row-Level Security (RLS)
- Never expose service role key in client
- Validate user input on both client and server
- Use environment variables for credentials
2. Performance
- Select only needed columns
- Use pagination for large datasets
- Add database indexes for frequent queries
- Reuse client instance (singleton pattern)
3. Type Safety
- Generate types from database schema
- Use TypeScript for all Supabase operations
- Leverage IDE autocomplete
4. Error Handling
- Always check error responses
- Use throwOnError() for promise rejection
- Implement proper error boundaries
5. Realtime
- Clean up subscriptions when components unmount
- Use filters to reduce unnecessary events
- Consider bandwidth for high-frequency updates
Resources
Official Documentation
- Supabase Docs
- JavaScript Client Reference
- Database Reference
- Auth Reference
- Storage Reference
- Realtime Reference
Community
Tools
Migration from Firebase
Supabase provides migration guides for Firebase users:
- PostgreSQL vs Firestore data modeling
- Auth migration (users, OAuth providers)
- Storage migration
- Realtime migration
- Cloud Functions → Edge Functions
See Firebase to Supabase Migration Guide
Support
For detailed implementation guidance:
- Read SKILL.md for comprehensive coverage
- Check EXAMPLES.md for practical code samples
- Consult REFERENCE.md for complete API details
For issues and questions:
---
Version: 1.0.0 Last Updated: October 2025 Maintained By: Claude Code Skills Team
Supabase API Reference
Complete reference documentation for Supabase JavaScript/TypeScript client.
Table of Contents
1. Client Configuration 2. Authentication API 3. Database API 4. Realtime API 5. Storage API 6. Error Handling 7. Type Definitions 8. Environment Variables 9. Performance Tuning 10. Security Checklist
Client Configuration
createClient()
Creates a new Supabase client instance.
Signature:
function createClient<Database = any>(
supabaseUrl: string,
supabaseKey: string,
options?: SupabaseClientOptions
): SupabaseClient<Database>Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
supabaseUrl | string | Yes | Your Supabase project URL |
supabaseKey | string | Yes | Your Supabase anon or service role key |
options | SupabaseClientOptions | No | Configuration options |
Options:
interface SupabaseClientOptions {
db?: {
schema: string // Default: 'public'
}
auth?: {
autoRefreshToken?: boolean // Default: true
persistSession?: boolean // Default: true
detectSessionInUrl?: boolean // Default: true
flowType?: 'pkce' | 'implicit' // Default: 'pkce'
storage?: Storage // Custom storage implementation
storageKey?: string // Default: 'sb-auth-token'
}
global?: {
headers?: Record<string, string>
fetch?: typeof fetch
}
realtime?: {
params?: {
eventsPerSecond?: number // Default: 10
}
timeout?: number // Default: 10000
heartbeatInterval?: number // Default: 30000
}
}Returns: SupabaseClient<Database>
Example:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://xyzcompany.supabase.co',
'public-anon-key',
{
auth: {
autoRefreshToken: true,
persistSession: true
},
global: {
headers: {
'X-Application-Name': 'MyApp'
}
}
}
)Authentication API
supabase.auth
Authentication namespace for all auth-related operations.
signUp()
Create a new user account.
Signature:
function signUp(credentials: {
email: string
password: string
options?: {
data?: object
emailRedirectTo?: string
captchaToken?: string
}
}): Promise<AuthResponse>Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | Yes | User's email address |
password | string | Yes | User's password |
options.data | object | No | Additional user metadata |
options.emailRedirectTo | string | No | URL to redirect after email confirmation |
options.captchaToken | string | No | Captcha token for bot protection |
Returns: Promise<AuthResponse>
type AuthResponse = {
data: {
user: User | null
session: Session | null
}
error: AuthError | null
}signInWithPassword()
Sign in with email and password.
Signature:
function signInWithPassword(credentials: {
email: string
password: string
}): Promise<AuthResponse>signInWithOtp()
Sign in with one-time password (email or phone).
Signature:
function signInWithOtp(credentials: {
email?: string
phone?: string
options?: {
emailRedirectTo?: string
shouldCreateUser?: boolean
data?: object
channel?: 'sms' | 'whatsapp'
}
}): Promise<AuthOtpResponse>signInWithOAuth()
Sign in with OAuth provider.
Signature:
function signInWithOAuth(credentials: {
provider: Provider
options?: {
redirectTo?: string
scopes?: string
queryParams?: Record<string, string>
}
}): Promise<OAuthResponse>Supported Providers:
apple,google,github,gitlab,bitbucketdiscord,facebook,twitter,microsoftlinkedin,notion,slack,spotify,twitchworkos,zoom, and more
signOut()
Sign out the current user.
Signature:
function signOut(): Promise<{ error: AuthError | null }>getSession()
Get the current session.
Signature:
function getSession(): Promise<{
data: { session: Session | null }
error: AuthError | null
}>getUser()
Get the current user.
Signature:
function getUser(jwt?: string): Promise<{
data: { user: User | null }
error: AuthError | null
}>updateUser()
Update user data.
Signature:
function updateUser(attributes: {
email?: string
password?: string
phone?: string
data?: object
}): Promise<UserResponse>refreshSession()
Manually refresh the session.
Signature:
function refreshSession(): Promise<AuthResponse>resetPasswordForEmail()
Send password reset email.
Signature:
function resetPasswordForEmail(
email: string,
options?: {
redirectTo?: string
captchaToken?: string
}
): Promise<{ data: object; error: AuthError | null }>onAuthStateChange()
Listen to auth state changes.
Signature:
function onAuthStateChange(
callback: (event: AuthChangeEvent, session: Session | null) => void
): { data: { subscription: Subscription } }Events:
SIGNED_IN- User signed inSIGNED_OUT- User signed outTOKEN_REFRESHED- Session token refreshedUSER_UPDATED- User data updatedPASSWORD_RECOVERY- Password recovery initiated
MFA Methods
enroll() - Enroll in MFA:
function enroll(params: {
factorType: 'totp'
friendlyName?: string
}): Promise<AuthMFAEnrollResponse>challenge() - Create MFA challenge:
function challenge(params: {
factorId: string
}): Promise<AuthMFAChallengeResponse>verify() - Verify MFA code:
function verify(params: {
factorId: string
challengeId?: string
code: string
}): Promise<AuthMFAVerifyResponse>unenroll() - Unenroll from MFA:
function unenroll(params: {
factorId: string
}): Promise<AuthMFAUnenrollResponse>listFactors() - List enrolled factors:
function listFactors(): Promise<{
data: { all: Factor[]; totp: Factor[] }
error: AuthError | null
}>Database API
supabase.from()
Create a query builder for a table.
Signature:
function from<T = any>(table: string): PostgrestQueryBuilder<T>SELECT Queries
select() - Fetch data:
function select(
columns?: string,
options?: {
head?: boolean
count?: 'exact' | 'planned' | 'estimated' | null
}
): PostgrestFilterBuilderFilter Methods:
| Method | Description | Example |
|---|---|---|
eq(column, value) | Equal to | .eq('status', 'active') |
neq(column, value) | Not equal to | .neq('role', 'admin') |
gt(column, value) | Greater than | .gt('age', 18) |
gte(column, value) | Greater than or equal | .gte('score', 80) |
lt(column, value) | Less than | .lt('price', 100) |
lte(column, value) | Less than or equal | .lte('stock', 10) |
like(column, pattern) | Pattern match | .like('email', '%@gmail.com') |
ilike(column, pattern) | Case-insensitive match | .ilike('name', '%john%') |
in(column, values) | In array | .in('id', [1, 2, 3]) |
is(column, value) | Is exact value (null) | .is('deleted_at', null) |
not(column, operator, value) | Negate condition | .not('status', 'eq', 'banned') |
or(query) | OR condition | .or('role.eq.admin,status.eq.vip') |
filter(column, operator, value) | Generic filter | .filter('age', 'gte', 18) |
Modifier Methods:
| Method | Description | Example |
|---|---|---|
order(column, options) | Order results | .order('created_at', { ascending: false }) |
limit(count) | Limit results | .limit(10) |
range(from, to) | Pagination | .range(0, 9) |
single() | Return single object | .select().eq('id', 1).single() |
maybeSingle() | Return single or null | .select().eq('id', 1).maybeSingle() |
csv() | Return as CSV | .select().csv() |
Joins:
// One-to-many
.select(`
id,
email,
posts (id, title)
`)
// Many-to-many
.select(`
id,
user_roles (
role:roles (name)
)
`)
// Inner join (only rows with relation)
.select(`
id,
posts!inner (id, title)
`)INSERT Operations
insert() - Insert rows:
function insert(
values: object | object[],
options?: {
defaultToNull?: boolean
}
): PostgrestFilterBuilderupsert() - Insert or update on conflict:
function upsert(
values: object | object[],
options?: {
onConflict?: string
ignoreDuplicates?: boolean
defaultToNull?: boolean
}
): PostgrestFilterBuilderUPDATE Operations
update() - Update rows:
function update(
values: object,
options?: {
count?: 'exact' | 'planned' | 'estimated' | null
}
): PostgrestFilterBuilderDELETE Operations
delete() - Delete rows:
function delete(
options?: {
count?: 'exact' | 'planned' | 'estimated' | null
}
): PostgrestFilterBuilderRPC Calls
rpc() - Call PostgreSQL function:
function rpc<T = any>(
fn: string,
params?: object,
options?: {
head?: boolean
count?: 'exact' | 'planned' | 'estimated' | null
}
): PostgrestFilterBuilder<T>Realtime API
supabase.channel()
Create a realtime channel.
Signature:
function channel(
name: string,
opts?: RealtimeChannelOptions
): RealtimeChannelOptions:
interface RealtimeChannelOptions {
config?: {
broadcast?: {
ack?: boolean
self?: boolean
}
presence?: {
key?: string
}
}
}Subscribe to Database Changes
channel.on(
'postgres_changes',
{
event: '*' | 'INSERT' | 'UPDATE' | 'DELETE'
schema: string
table: string
filter?: string
},
callback: (payload: RealtimePostgresChangesPayload) => void
)Payload Structure:
interface RealtimePostgresChangesPayload<T = any> {
eventType: 'INSERT' | 'UPDATE' | 'DELETE'
new: T // New record (INSERT, UPDATE)
old: T // Old record (UPDATE, DELETE)
schema: string
table: string
commit_timestamp: string
errors: any[]
}Broadcast Messages
Send:
channel.send({
type: 'broadcast'
event: string
payload: any
})Receive:
channel.on(
'broadcast',
{ event: string },
callback: (payload: any) => void
)Presence Tracking
Track presence:
channel.track(state: object): Promise<'ok' | 'timed_out' | 'error'>Get presence state:
channel.presenceState(): Record<string, Presence[]>Listen to presence:
channel.on(
'presence',
{ event: 'sync' | 'join' | 'leave' },
callback: (payload: PresencePayload) => void
)Untrack presence:
channel.untrack(): Promise<'ok' | 'timed_out' | 'error'>Channel Lifecycle
subscribe() - Subscribe to channel:
channel.subscribe(
callback?: (status: 'SUBSCRIBED' | 'TIMED_OUT' | 'CLOSED' | 'CHANNEL_ERROR') => void
): RealtimeChannelunsubscribe() - Unsubscribe from channel:
channel.unsubscribe(): Promise<'ok' | 'timed_out' | 'error'>Storage API
supabase.storage
Storage namespace for file operations.
Bucket Management
listBuckets() - List all buckets:
function listBuckets(): Promise<{
data: Bucket[] | null
error: StorageError | null
}>getBucket() - Get bucket details:
function getBucket(id: string): Promise<{
data: Bucket | null
error: StorageError | null
}>createBucket() - Create bucket:
function createBucket(
id: string,
options?: {
public?: boolean
fileSizeLimit?: number
allowedMimeTypes?: string[]
}
): Promise<{
data: { name: string } | null
error: StorageError | null
}>updateBucket() - Update bucket:
function updateBucket(
id: string,
options: {
public?: boolean
fileSizeLimit?: number
allowedMimeTypes?: string[]
}
): Promise<{
data: { message: string } | null
error: StorageError | null
}>deleteBucket() - Delete bucket:
function deleteBucket(id: string): Promise<{
data: { message: string } | null
error: StorageError | null
}>emptyBucket() - Remove all files from bucket:
function emptyBucket(id: string): Promise<{
data: { message: string } | null
error: StorageError | null
}>File Operations
from() - Access bucket:
function from(id: string): StorageFileApiupload() - Upload file:
function upload(
path: string,
fileBody: File | Blob | ArrayBuffer | FormData,
options?: {
cacheControl?: string
contentType?: string
upsert?: boolean
duplex?: string
onUploadProgress?: (progress: { loaded: number; total: number }) => void
}
): Promise<{
data: { path: string; id: string; fullPath: string } | null
error: StorageError | null
}>download() - Download file:
function download(path: string): Promise<{
data: Blob | null
error: StorageError | null
}>list() - List files:
function list(
path?: string,
options?: {
limit?: number
offset?: number
sortBy?: {
column: 'name' | 'created_at' | 'updated_at' | 'last_accessed_at'
order: 'asc' | 'desc'
}
search?: string
}
): Promise<{
data: FileObject[] | null
error: StorageError | null
}>remove() - Delete files:
function remove(paths: string[]): Promise<{
data: FileObject[] | null
error: StorageError | null
}>move() - Move file:
function move(
fromPath: string,
toPath: string
): Promise<{
data: { message: string } | null
error: StorageError | null
}>copy() - Copy file:
function copy(
fromPath: string,
toPath: string
): Promise<{
data: { path: string } | null
error: StorageError | null
}>URL Generation
getPublicUrl() - Get public URL:
function getPublicUrl(
path: string,
options?: {
download?: boolean | string
transform?: {
width?: number
height?: number
resize?: 'cover' | 'contain' | 'fill'
format?: 'origin' | 'webp' | 'avif'
quality?: number
}
}
): {
data: { publicUrl: string }
}createSignedUrl() - Create signed URL:
function createSignedUrl(
path: string,
expiresIn: number,
options?: {
download?: boolean | string
transform?: {
width?: number
height?: number
resize?: 'cover' | 'contain' | 'fill'
format?: 'origin' | 'webp' | 'avif'
quality?: number
}
}
): Promise<{
data: { signedUrl: string; path: string } | null
error: StorageError | null
}>createSignedUrls() - Create multiple signed URLs:
function createSignedUrls(
paths: string[],
expiresIn: number,
options?: {
download?: boolean | string
}
): Promise<{
data: Array<{
path: string
signedUrl: string
error: string | null
}> | null
error: StorageError | null
}>Error Handling
Error Types
AuthError:
interface AuthError extends Error {
status?: number
code?: string
}PostgrestError:
interface PostgrestError {
message: string
details: string
hint: string
code: string
}StorageError:
interface StorageError extends Error {
statusCode?: string
}Response Pattern
All Supabase operations follow this pattern:
type SupabaseResponse<T> = {
data: T | null
error: Error | null
}Error Handling Strategies
Option 1: Check error field
const { data, error } = await supabase
.from('users')
.select()
if (error) {
console.error('Error:', error.message)
return
}
// Use data safely
console.log(data)Option 2: Use throwOnError()
try {
const { data } = await supabase
.from('users')
.insert({ email: 'user@example.com' })
.throwOnError()
console.log('Success:', data)
} catch (error) {
console.error('Failed:', error)
}Common Error Codes
Authentication:
400- Invalid credentials422- Email not confirmed429- Too many requests
Database:
23505- Unique violation23503- Foreign key violation42501- Insufficient privilege (RLS)
Storage:
404- File not found413- Payload too large415- Unsupported media type
Type Definitions
User
interface User {
id: string
app_metadata: { [key: string]: any }
user_metadata: { [key: string]: any }
aud: string
confirmation_sent_at?: string
recovery_sent_at?: string
email_change_sent_at?: string
new_email?: string
invited_at?: string
action_link?: string
email?: string
phone?: string
created_at: string
confirmed_at?: string
email_confirmed_at?: string
phone_confirmed_at?: string
last_sign_in_at?: string
role?: string
updated_at?: string
identities?: UserIdentity[]
}Session
interface Session {
access_token: string
refresh_token: string
expires_in: number
expires_at?: number
token_type: string
user: User
}FileObject
interface FileObject {
name: string
id: string | null
updated_at: string | null
created_at: string | null
last_accessed_at: string | null
metadata: {
eTag: string
size: number
mimetype: string
cacheControl: string
lastModified: string
contentLength: number
httpStatusCode: number
}
}Environment Variables
Required Variables
# Client-side (safe to expose)
NEXT_PUBLIC_SUPABASE_URL=https://xyzcompany.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# Server-side only (keep secure!)
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Optional Variables
# Database connection (for direct PostgreSQL access)
SUPABASE_DB_URL=postgresql://postgres:[PASSWORD]@db.xyzcompany.supabase.co:5432/postgres
# JWT secret (for custom JWT verification)
SUPABASE_JWT_SECRET=your-jwt-secret-herePerformance Tuning
Database Optimization
1. Add Indexes:
-- Index frequently queried columns
CREATE INDEX idx_users_email ON users(email);
-- Index foreign keys
CREATE INDEX idx_posts_user_id ON posts(user_id);
-- Composite index for multi-column queries
CREATE INDEX idx_posts_user_published ON posts(user_id, published) WHERE published = true;
-- Partial index for common filters
CREATE INDEX idx_active_users ON users(status) WHERE status = 'active';2. Use Appropriate Query Patterns:
// Bad: Fetch all columns
const { data } = await supabase.from('users').select()
// Good: Only fetch needed columns
const { data } = await supabase
.from('users')
.select('id, email')3. Implement Pagination:
// Bad: Fetch all rows
const { data } = await supabase.from('posts').select()
// Good: Paginate results
const { data } = await supabase
.from('posts')
.select()
.range(0, 9)
.order('created_at', { ascending: false })4. Use Count Wisely:
// For large tables, use estimated count
const { count } = await supabase
.from('users')
.select('*', { count: 'estimated', head: true })
// For exact count only when necessary
const { count } = await supabase
.from('small_table')
.select('*', { count: 'exact', head: true })Realtime Optimization
1. Use Filters:
// Bad: Subscribe to all changes
channel.on('postgres_changes', { event: '*', schema: 'public', table: 'posts' }, handler)
// Good: Filter by relevant rows
channel.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'posts', filter: `user_id=eq.${userId}` },
handler
)2. Clean Up Subscriptions:
useEffect(() => {
const channel = supabase.channel('my-channel')
channel.subscribe()
// Always clean up
return () => {
channel.unsubscribe()
}
}, [])Storage Optimization
1. Use Image Transformations:
// Generate responsive images
const thumbnailUrl = supabase.storage
.from('photos')
.getPublicUrl('photo.jpg', {
transform: {
width: 200,
height: 200,
quality: 80,
format: 'webp'
}
})2. Set Appropriate Cache Headers:
await supabase.storage
.from('assets')
.upload('file.jpg', file, {
cacheControl: '31536000' // 1 year
})Security Checklist
Authentication
- [ ] Never expose service role key in client-side code
- [ ] Use anon key for client applications
- [ ] Enable email confirmation for sign-ups
- [ ] Implement rate limiting for auth endpoints
- [ ] Use PKCE flow for OAuth
- [ ] Enable MFA for sensitive applications
- [ ] Set appropriate password requirements
- [ ] Implement account lockout after failed attempts
Database
- [ ] Enable Row-Level Security (RLS) on all tables
- [ ] Create appropriate RLS policies for each table
- [ ] Test RLS policies with different user contexts
- [ ] Never bypass RLS in client-side code
- [ ] Use prepared statements (automatic with Supabase)
- [ ] Validate user input on both client and server
- [ ] Implement database constraints (NOT NULL, UNIQUE, CHECK)
- [ ] Use database functions for complex operations
Storage
- [ ] Set appropriate bucket policies
- [ ] Use RLS for storage.objects table
- [ ] Validate file types and sizes
- [ ] Scan uploaded files for malware
- [ ] Use signed URLs for private files
- [ ] Set appropriate CORS policies
- [ ] Implement file size limits
- [ ] Use CDN for public assets
General
- [ ] Use environment variables for all secrets
- [ ] Never commit credentials to version control
- [ ] Implement proper error handling (don't leak sensitive info)
- [ ] Use HTTPS for all connections
- [ ] Enable database backups
- [ ] Monitor auth logs for suspicious activity
- [ ] Keep Supabase client library updated
- [ ] Implement proper CORS configuration
- [ ] Use Content Security Policy (CSP) headers
- [ ] Regularly audit RLS policies
---
Reference Version: 1.0.0 Last Updated: October 2025 For Examples: See EXAMPLES.md For Guides: See SKILL.md