Supabase Client Patterns
Common usage patterns for CRUD operations, real-time features and file storage
Last updated: 8/15/2025
Once your Supabase client is set up, these patterns cover the most common operations you'll need in your applications.
Basic CRUD Operations
Reading Data
// Get all records
const { data: posts, error } = await supabase
.from('posts')
.select('*');
// Get specific columns
const { data: posts } = await supabase
.from('posts')
.select('id, title, created_at');
// Get single record
const { data: post } = await supabase
.from('posts')
.select('*')
.eq('id', postId)
.single();
// Get with filtering
const { data: publishedPosts } = await supabase
.from('posts')
.select('*')
.eq('published', true)
.order('created_at', { ascending: false });
Creating Data
// Insert single record
const { data: newPost, error } = await supabase
.from('posts')
.insert({
title: 'New Post',
content: 'Post content here...',
author_id: user.id
})
.select();
// Insert multiple records
const { data: newPosts } = await supabase
.from('posts')
.insert([
{ title: 'Post 1', author_id: user.id },
{ title: 'Post 2', author_id: user.id }
])
.select();
// Insert with conflict handling
const { data: post } = await supabase
.from('posts')
.upsert({
id: existingId,
title: 'Updated Title',
author_id: user.id
})
.select();
Updating Data
// Update specific record
const { data: updatedPost } = await supabase
.from('posts')
.update({
title: 'Updated Title',
updated_at: new Date().toISOString()
})
.eq('id', postId)
.select();
// Update multiple records
const { data: updatedPosts } = await supabase
.from('posts')
.update({ published: true })
.eq('author_id', user.id)
.select();
// Conditional update
const { data: post } = await supabase
.from('posts')
.update({ view_count: 'view_count + 1' })
.eq('id', postId)
.eq('published', true)
.select();
Deleting Data
// Delete specific record
const { error } = await supabase
.from('posts')
.delete()
.eq('id', postId);
// Delete with conditions
const { data: deletedPosts } = await supabase
.from('posts')
.delete()
.eq('author_id', user.id)
.eq('published', false)
.select();
Working with Relationships
Fetching Related Data
// Get posts with author information
const { data: posts } = await supabase
.from('posts')
.select(`
*,
profiles (
id,
name,
avatar_url
)
`);
// Nested relationships
const { data: posts } = await supabase
.from('posts')
.select(`
id,
title,
profiles (name),
comments (
id,
content,
profiles (name)
)
`);
// Filter by related data
const { data: posts } = await supabase
.from('posts')
.select(`
*,
profiles!inner (name)
`)
.eq('profiles.name', 'John Doe');
Creating Related Records
// Create post with tags (many-to-many)
const { data: post } = await supabase
.from('posts')
.insert({
title: 'New Post',
content: 'Content here...',
author_id: user.id
})
.select()
.single();
// Create the tag relationships
const { data: postTags } = await supabase
.from('post_tags')
.insert([
{ post_id: post.id, tag_id: 'tag-1' },
{ post_id: post.id, tag_id: 'tag-2' }
]);
Real-time Subscriptions
Basic Subscription
// Listen to all changes on a table
const subscription = supabase
.channel('posts_changes')
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'posts' },
(payload) => {
console.log('Change received!', payload);
}
)
.subscribe();
// Clean up subscription
const unsubscribe = () => {
supabase.removeChannel(subscription);
};
Filtered Subscriptions
// Only listen to changes for current user's posts
const userPostsSubscription = supabase
.channel('user_posts')
.on('postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts',
filter: `author_id=eq.${user.id}`
},
(payload) => {
console.log('User post changed:', payload);
}
)
.subscribe();
React Integration
import { useEffect, useState } from 'react';
function PostsList() {
const [posts, setPosts] = useState([]);
useEffect(() => {
// Initial load
const loadPosts = async () => {
const { data } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false });
setPosts(data || []);
};
loadPosts();
// Set up real-time subscription
const subscription = supabase
.channel('posts_realtime')
.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'posts' },
(payload) => {
setPosts(prev => [payload.new, ...prev]);
}
)
.on('postgres_changes',
{ event: 'UPDATE', schema: 'public', table: 'posts' },
(payload) => {
setPosts(prev =>
prev.map(post =>
post.id === payload.new.id ? payload.new : post
)
);
}
)
.on('postgres_changes',
{ event: 'DELETE', schema: 'public', table: 'posts' },
(payload) => {
setPosts(prev =>
prev.filter(post => post.id !== payload.old.id)
);
}
)
.subscribe();
return () => {
supabase.removeChannel(subscription);
};
}, []);
return (
<div>
{posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
File Storage
Upload Files
// Upload file to storage bucket
const uploadFile = async (file, bucket = 'avatars') => {
const fileExt = file.name.split('.').pop();
const fileName = `${user.id}/${Date.now()}.${fileExt}`;
const { data, error } = await supabase.storage
.from(bucket)
.upload(fileName, file);
if (error) throw error;
return data;
};
// Upload with progress tracking
const uploadWithProgress = async (file) => {
const { data, error } = await supabase.storage
.from('documents')
.upload(`docs/${file.name}`, file, {
onUploadProgress: (progress) => {
console.log(`Upload progress: ${progress.loaded}/${progress.total}`);
}
});
return { data, error };
};
Download and Access Files
// Get public URL (for public buckets)
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('user-avatar.jpg');
console.log('Public URL:', data.publicUrl);
// Get signed URL (for private buckets)
const { data: signedUrl } = await supabase.storage
.from('private-docs')
.createSignedUrl('document.pdf', 60); // Valid for 60 seconds
// Download file directly
const { data: fileBlob } = await supabase.storage
.from('documents')
.download('path/to/file.pdf');
Storage Helpers
// Delete file
const { error } = await supabase.storage
.from('avatars')
.remove(['old-avatar.jpg']);
// List files in folder
const { data: files } = await supabase.storage
.from('documents')
.list('user-uploads/', {
limit: 100,
offset: 0
});
// Move/rename file
const { data, error } = await supabase.storage
.from('documents')
.move('old-path/file.pdf', 'new-path/file.pdf');
Authentication Patterns
Sign Up and Sign In
// Email/password signup
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'password123',
options: {
data: {
name: 'John Doe',
avatar_url: 'https://yourdomain.com/avatar.jpg'
}
}
});
// Email/password signin
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'password123'
});
// Magic link signin
const { error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: {
emailRedirectTo: 'https://yourdomain.com/dashboard'
}
});
OAuth Providers
// Google OAuth
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://yourdomain.com/auth/callback'
}
});
// GitHub OAuth
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github'
});
Session Management
// Get current user
const { data: { user } } = await supabase.auth.getUser();
// Listen to auth changes
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') {
console.log('User signed in:', session.user);
} else if (event === 'SIGNED_OUT') {
console.log('User signed out');
}
});
// Sign out
const { error } = await supabase.auth.signOut();
Edge Functions
Calling Edge Functions
// Call edge function
const { data, error } = await supabase.functions.invoke('send-email', {
body: {
to: 'user@example.com',
subject: 'Welcome!',
message: 'Thanks for signing up'
}
});
// Call with custom headers
const { data, error } = await supabase.functions.invoke('process-payment', {
body: { amount: 1000, currency: 'USD' },
headers: {
'Authorization': `Bearer ${customToken}`
}
});
Error Handling Patterns
Comprehensive Error Handling
const handleSupabaseOperation = async (operation) => {
try {
const { data, error } = await operation();
if (error) {
// Supabase-specific errors
switch (error.code) {
case '23505': // Unique constraint violation
throw new Error('That email is already registered');
case '23503': // Foreign key violation
throw new Error('Referenced record does not exist');
case 'PGRST116': // No rows returned
throw new Error('Record not found');
default:
throw new Error(error.message);
}
}
return data;
} catch (err) {
console.error('Database operation failed:', err);
throw err;
}
};
// Usage
try {
const posts = await handleSupabaseOperation(() =>
supabase.from('posts').select('*')
);
} catch (error) {
setErrorMessage(error.message);
}
Performance Optimization
Efficient Queries
// Good: Only select needed columns
const { data } = await supabase
.from('posts')
.select('id, title, created_at')
.limit(10);
// Good: Use single() for one record
const { data: post } = await supabase
.from('posts')
.select('*')
.eq('slug', postSlug)
.single();
// Good: Count without fetching all data
const { count } = await supabase
.from('posts')
.select('*', { count: 'exact', head: true });
Caching Strategies
// Simple in-memory cache
const cache = new Map();
const getCachedPosts = async () => {
const cacheKey = 'recent-posts';
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const { data: posts } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false })
.limit(10);
cache.set(cacheKey, posts);
// Clear cache after 5 minutes
setTimeout(() => cache.delete(cacheKey), 5 * 60 * 1000);
return posts;
};
These patterns cover the majority of Supabase client operations you'll need. Combine them to build robust, real-time applications with minimal backend code.
Related Topics
Continue building your Supabase knowledge:
- Security Leak Prevention - Preventing API key leaks
- Security Incident Response - Emergency procedures for leaks
- Multi-tenant Security Patterns - Team and organisation data isolation
- API Keys and Environment Setup - Safe handling of credentials