Learn

Navigate through learn topics

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: