Skip to main content

Overview

Supabase Storage provides S3-compatible object storage for user uploads. AniDojo uses Storage for profile avatars, anime images, and review attachments.

Required Storage Buckets

AniDojo uses three storage buckets:
  • avatars - User profile pictures
  • anime-images - Custom anime cover art (optional)
  • review-images - Review image attachments (optional)

Creating Storage Buckets

1

Navigate to Storage

In your Supabase dashboard, click Storage in the left sidebar.
2

Create avatars bucket

Click New bucket and configure:
  • Name: avatars
  • Public bucket: ✅ Enabled
  • File size limit: 5 MB
  • Allowed MIME types: image/jpeg, image/png, image/webp, image/gif
Click Create bucket
3

Create anime-images bucket (optional)

Click New bucket and configure:
  • Name: anime-images
  • Public bucket: ✅ Enabled
  • File size limit: 10 MB
  • Allowed MIME types: image/jpeg, image/png, image/webp
4

Create review-images bucket (optional)

Click New bucket and configure:
  • Name: review-images
  • Public bucket: ✅ Enabled
  • File size limit: 10 MB
  • Allowed MIME types: image/jpeg, image/png, image/webp
Public buckets allow anyone to view files, but Storage Policies control who can upload, update, or delete files.

Storage Policies

After creating buckets, set up Row Level Security policies to control access.

Avatars Bucket Policies

Navigate to StoragePoliciesavatars and create these policies:
Allow anyone to view avatar images:
CREATE POLICY "Avatar images are publicly accessible"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars');
Users can only upload to their own user ID folder:
CREATE POLICY "Users can upload their own avatar"
ON storage.objects FOR INSERT
WITH CHECK (
  bucket_id = 'avatars' AND
  auth.uid()::text = (storage.foldername(name))[1]
);
This ensures users upload to paths like: {user_id}/avatar.jpg
CREATE POLICY "Users can update their own avatar"
ON storage.objects FOR UPDATE
USING (
  bucket_id = 'avatars' AND
  auth.uid()::text = (storage.foldername(name))[1]
);
CREATE POLICY "Users can delete their own avatar"
ON storage.objects FOR DELETE
USING (
  bucket_id = 'avatars' AND
  auth.uid()::text = (storage.foldername(name))[1]
);

Complete Avatars Policies SQL

-- Allow public read access
CREATE POLICY "Avatar images are publicly accessible"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars');

-- Allow authenticated users to upload their own avatars
CREATE POLICY "Users can upload their own avatar"
ON storage.objects FOR INSERT
WITH CHECK (
  bucket_id = 'avatars' AND
  auth.uid()::text = (storage.foldername(name))[1]
);

-- Allow users to update their own avatars
CREATE POLICY "Users can update their own avatar"
ON storage.objects FOR UPDATE
USING (
  bucket_id = 'avatars' AND
  auth.uid()::text = (storage.foldername(name))[1]
);

-- Allow users to delete their own avatars
CREATE POLICY "Users can delete their own avatar"
ON storage.objects FOR DELETE
USING (
  bucket_id = 'avatars' AND
  auth.uid()::text = (storage.foldername(name))[1]
);

Anime Images Bucket Policies

-- Allow public read access
CREATE POLICY "Anime images are publicly accessible"
ON storage.objects FOR SELECT
USING (bucket_id = 'anime-images');

-- Allow authenticated users to upload anime images
CREATE POLICY "Authenticated users can upload anime images"
ON storage.objects FOR INSERT
WITH CHECK (
  bucket_id = 'anime-images' AND
  auth.role() = 'authenticated'
);

Review Images Bucket Policies

-- Allow public read access
CREATE POLICY "Review images are publicly accessible"
ON storage.objects FOR SELECT
USING (bucket_id = 'review-images');

-- Allow authenticated users to upload review images
CREATE POLICY "Authenticated users can upload review images"
ON storage.objects FOR INSERT
WITH CHECK (
  bucket_id = 'review-images' AND
  auth.role() = 'authenticated'
);

Using Storage in Your App

AniDojo provides helper functions in src/lib/supabase/storage.ts:

Upload Avatar Example

import { uploadAvatar, deleteAvatar } from '@/lib/supabase/storage';
import { createClient } from '@/lib/supabase/client';

async function handleAvatarUpload(file: File, userId: string) {
  try {
    // Upload the avatar
    const avatarUrl = await uploadAvatar(userId, file);
    
    // Update profile in database
    const supabase = createClient();
    const { error } = await supabase
      .from('profiles')
      .update({ avatar_url: avatarUrl })
      .eq('id', userId);
    
    if (error) throw error;
    
    return avatarUrl;
  } catch (error) {
    console.error('Error uploading avatar:', error);
    throw error;
  }
}

Upload Generic File

import { uploadFile, getPublicUrl, STORAGE_BUCKETS } from '@/lib/supabase/storage';

async function uploadReviewImage(file: File) {
  const fileName = `${Date.now()}-${file.name}`;
  const filePath = `reviews/${fileName}`;
  
  try {
    // Upload the file
    await uploadFile(STORAGE_BUCKETS.REVIEW_IMAGES, filePath, file);
    
    // Get the public URL
    const url = getPublicUrl(STORAGE_BUCKETS.REVIEW_IMAGES, filePath);
    
    return url;
  } catch (error) {
    console.error('Error uploading image:', error);
    throw error;
  }
}

Delete File

import { deleteFile, STORAGE_BUCKETS } from '@/lib/supabase/storage';

async function deleteReviewImage(filePath: string) {
  try {
    await deleteFile(STORAGE_BUCKETS.REVIEW_IMAGES, filePath);
  } catch (error) {
    console.error('Error deleting image:', error);
    throw error;
  }
}

List Files in Bucket

import { listFiles, STORAGE_BUCKETS } from '@/lib/supabase/storage';

async function getUserAvatars(userId: string) {
  try {
    const files = await listFiles(STORAGE_BUCKETS.AVATARS, userId, {
      limit: 10,
      sortBy: { column: 'created_at', order: 'desc' }
    });
    
    return files;
  } catch (error) {
    console.error('Error listing files:', error);
    throw error;
  }
}

Storage Helper Functions Reference

All storage functions are in src/lib/supabase/storage.ts:
Upload a file to any bucket (client-side):
uploadFile(
  bucket: string,
  path: string,
  file: File,
  options?: {
    cacheControl?: string;
    contentType?: string;
    upsert?: boolean;
  }
)
Upload a file from server-side code:
uploadFileServer(
  bucket: string,
  path: string,
  file: File | Buffer,
  options?: {
    cacheControl?: string;
    contentType?: string;
    upsert?: boolean;
  }
)
Get the public URL for a file:
getPublicUrl(bucket: string, path: string): string
Get a temporary signed URL (for private files):
getSignedUrl(
  bucket: string,
  path: string,
  expiresIn: number = 3600
): Promise<string>
Delete a file from storage:
deleteFile(bucket: string, path: string): Promise<void>
Convenience function for avatar uploads:
uploadAvatar(userId: string, file: File): Promise<string>
Returns the public URL of the uploaded avatar.
Delete a user’s avatar by URL:
deleteAvatar(userId: string, avatarUrl: string): Promise<void>

Storage Bucket Constants

import { STORAGE_BUCKETS } from '@/lib/supabase/storage';

// Available buckets:
STORAGE_BUCKETS.AVATARS        // 'avatars'
STORAGE_BUCKETS.ANIME_IMAGES   // 'anime-images'
STORAGE_BUCKETS.REVIEW_IMAGES  // 'review-images'

File Upload Best Practices

Always validate files before uploading:
  • Check file size limits
  • Verify MIME type
  • Sanitize file names
  • Handle errors gracefully

Client-Side Validation Example

function validateImageFile(file: File): { valid: boolean; error?: string } {
  // Check file size (5MB limit for avatars)
  const maxSize = 5 * 1024 * 1024; // 5MB in bytes
  if (file.size > maxSize) {
    return { valid: false, error: 'File size must be less than 5MB' };
  }
  
  // Check file type
  const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
  if (!allowedTypes.includes(file.type)) {
    return { valid: false, error: 'File must be JPEG, PNG, WebP, or GIF' };
  }
  
  return { valid: true };
}

// Usage
const validation = validateImageFile(file);
if (!validation.valid) {
  alert(validation.error);
  return;
}

Testing Storage Setup

Test your storage configuration:
1

Upload a test file

Use the Supabase dashboard to manually upload a test image to the avatars bucket.
2

Verify public access

Get the public URL and open it in your browser to verify the image loads.
3

Test policy enforcement

Try uploading to another user’s folder - it should be rejected by RLS policies.
4

Test from your app

Use the uploadAvatar() function in your app to upload a real profile picture.

Troubleshooting

Upload fails with “Policy violation”

  • Verify the user is authenticated
  • Check the file path matches the policy (e.g., {user_id}/filename.jpg)
  • Confirm storage policies are created correctly
  • View detailed error in browser console

Image doesn’t load

  • Verify the bucket is set to Public
  • Check the file exists in: Storageavatars
  • Confirm the URL format is correct
  • Check browser console for CORS errors

File size limit exceeded

  • Configure bucket file size limit in: StoragePoliciesConfiguration
  • Implement client-side validation before upload
  • Consider image compression before upload

Wrong MIME type

  • Set allowed MIME types in bucket configuration
  • Validate file type on client side before upload
  • Check the Content-Type header in the upload request

Next Steps

Authentication Setup

Configure user authentication and email templates

API Integration

Connect to the Jikan anime API

Resources

Build docs developers (and LLMs) love