Supabase Storage: Complete Guide to File Uploads and Management
Master Supabase Storage with this comprehensive guide. Learn file uploads, image optimization, CDN delivery, security policies, and advanced patterns for production applications.
Supabase Storage: Complete Guide to File Uploads and Management#
Supabase Storage provides S3-compatible object storage with built-in CDN, image transformations, and security policies. This guide covers everything from basic uploads to production-ready patterns.
Architecture Overview#
Client → Supabase Storage API → S3-Compatible Storage → CDN
↓ ↓
Upload Fast Delivery
↓ ↓
RLS Policies ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ←
Setup and Configuration#
1. Create Storage Buckets#
-- Create a public bucket for avatars
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true);
-- Create a private bucket for documents
INSERT INTO storage.buckets (id, name, public)
VALUES ('documents', 'documents', false);
2. Storage Policies#
-- Allow users to upload their own avatar
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 read their own avatar
CREATE POLICY "Users can read their own avatar"
ON storage.objects FOR SELECT
USING (
bucket_id = 'avatars' AND
auth.uid()::text = (storage.foldername(name))[1]
);
-- Allow users to update their own avatar
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 avatar
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]
);
Basic File Upload#
Simple Upload Component#
'use client';
import { useState } from 'react';
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
export default function FileUpload() {
const [uploading, setUploading] = useState(false);
const [fileUrl, setFileUrl] = useState<string | null>(null);
const supabase = createClientComponentClient();
const uploadFile = async (event: React.ChangeEvent<HTMLInputElement>) => {
try {
setUploading(true);
if (!event.target.files || event.target.files.length === 0) {
throw new Error('You must select a file to upload.');
}
const file = event.target.files[0];
const fileExt = file.name.split('.').pop();
const fileName = `${Math.random()}.${fileExt}`;
const filePath = `${fileName}`;
const { error: uploadError } = await supabase.storage
.from('avatars')
.upload(filePath, file);
if (uploadError) {
throw uploadError;
}
// Get public URL
const { data } = supabase.storage
.from('avatars')
.getPublicUrl(filePath);
setFileUrl(data.publicUrl);
} catch (error) {
alert(error.message);
} finally {
setUploading(false);
}
};
return (
<div>
<input
type="file"
onChange={uploadFile}
disabled={uploading}
/>
{uploading && <p>Uploading...</p>}
{fileUrl && (
<div>
<p>File uploaded successfully!</p>
<img src={fileUrl} alt="Uploaded file" />
</div>
)}
</div>
);
}
Advanced Upload Patterns#
1. Avatar Upload with Preview#
'use client';
import { useState, useRef } from 'react';
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
import Image from 'next/image';
export default function AvatarUpload({ userId, currentAvatar }) {
const [avatar, setAvatar] = useState(currentAvatar);
const [uploading, setUploading] = useState(false);
const [preview, setPreview] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const supabase = createClientComponentClient();
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
// Validate file type
if (!file.type.startsWith('image/')) {
alert('Please select an image file');
return;
}
// Validate file size (max 2MB)
if (file.size > 2 * 1024 * 1024) {
alert('File size must be less than 2MB');
return;
}
// Create preview
const reader = new FileReader();
reader.onloadend = () => {
setPreview(reader.result as string);
};
reader.readAsDataURL(file);
};
const uploadAvatar = async () => {
try {
setUploading(true);
const file = fileInputRef.current?.files?.[0];
if (!file) return;
// Delete old avatar if exists
if (avatar) {
const oldPath = avatar.split('/').pop();
await supabase.storage
.from('avatars')
.remove([`${userId}/${oldPath}`]);
}
// Upload new avatar
const fileExt = file.name.split('.').pop();
const fileName = `${Date.now()}.${fileExt}`;
const filePath = `${userId}/${fileName}`;
const { error: uploadError } = await supabase.storage
.from('avatars')
.upload(filePath, file, {
cacheControl: '3600',
upsert: false
});
if (uploadError) throw uploadError;
// Get public URL
const { data } = supabase.storage
.from('avatars')
.getPublicUrl(filePath);
// Update user profile
const { error: updateError } = await supabase
.from('profiles')
.update({ avatar_url: data.publicUrl })
.eq('id', userId);
if (updateError) throw updateError;
setAvatar(data.publicUrl);
setPreview(null);
alert('Avatar updated successfully!');
} catch (error) {
alert(error.message);
} finally {
setUploading(false);
}
};
return (
<div className="flex flex-col items-center gap-4">
<div className="relative w-32 h-32 rounded-full overflow-hidden bg-gray-200">
{(preview || avatar) && (
<Image
src={preview || avatar}
alt="Avatar"
fill
className="object-cover"
/>
)}
</div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileSelect}
className="hidden"
/>
<div className="flex gap-2">
<button
onClick={() => fileInputRef.current?.click()}
disabled={uploading}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Choose File
</button>
{preview && (
<button
onClick={uploadAvatar}
disabled={uploading}
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
>
{uploading ? 'Uploading...' : 'Upload'}
</button>
)}
</div>
</div>
);
}
2. Drag and Drop Upload#
'use client';
import { useState, useCallback } from 'react';
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
import { Upload } from 'lucide-react';
export default function DragDropUpload({ userId, onUploadComplete }) {
const [isDragging, setIsDragging] = useState(false);
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const supabase = createClientComponentClient();
const handleDrag = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
}, []);
const handleDragIn = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
}, []);
const handleDragOut = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
}, []);
const handleDrop = useCallback(async (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
const files = Array.from(e.dataTransfer.files);
await uploadFiles(files);
}, []);
const uploadFiles = async (files: File[]) => {
setUploading(true);
const uploadedUrls: string[] = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const fileExt = file.name.split('.').pop();
const fileName = `${Date.now()}-${i}.${fileExt}`;
const filePath = `${userId}/${fileName}`;
try {
const { error } = await supabase.storage
.from('documents')
.upload(filePath, file, {
cacheControl: '3600',
upsert: false
});
if (error) throw error;
const { data } = supabase.storage
.from('documents')
.getPublicUrl(filePath);
uploadedUrls.push(data.publicUrl);
setProgress(((i + 1) / files.length) * 100);
} catch (error) {
console.error('Upload error:', error);
}
}
setUploading(false);
setProgress(0);
onUploadComplete(uploadedUrls);
};
return (
<div
onDragEnter={handleDragIn}
onDragLeave={handleDragOut}
onDragOver={handleDrag}
onDrop={handleDrop}
className={`
border-2 border-dashed rounded-lg p-8 text-center
transition-colors cursor-pointer
${isDragging ? 'border-blue-500 bg-blue-50' : 'border-gray-300'}
${uploading ? 'opacity-50 pointer-events-none' : ''}
`}
>
<Upload className="w-12 h-12 mx-auto mb-4 text-gray-400" />
{uploading ? (
<div>
<p className="text-gray-600 mb-2">Uploading...</p>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full transition-all"
style={{ width: `${progress}%` }}
/>
</div>
</div>
) : (
<div>
<p className="text-gray-600 mb-2">
Drag and drop files here, or click to select
</p>
<p className="text-sm text-gray-400">
Supports: Images, PDFs, Documents
</p>
</div>
)}
</div>
);
}
3. Multiple File Upload with Progress#
'use client';
import { useState } from 'react';
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
import { X, CheckCircle, AlertCircle } from 'lucide-react';
interface UploadFile {
file: File;
progress: number;
status: 'pending' | 'uploading' | 'success' | 'error';
url?: string;
error?: string;
}
export default function MultiFileUpload({ userId }) {
const [files, setFiles] = useState<UploadFile[]>([]);
const supabase = createClientComponentClient();
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFiles = Array.from(e.target.files || []);
const newFiles: UploadFile[] = selectedFiles.map(file => ({
file,
progress: 0,
status: 'pending'
}));
setFiles(prev => [...prev, ...newFiles]);
};
const uploadFile = async (index: number) => {
const fileData = files[index];
setFiles(prev => prev.map((f, i) =>
i === index ? { ...f, status: 'uploading' } : f
));
try {
const file = fileData.file;
const fileExt = file.name.split('.').pop();
const fileName = `${Date.now()}-${file.name}`;
const filePath = `${userId}/${fileName}`;
// Simulate progress (in real app, use XMLHttpRequest for actual progress)
const progressInterval = setInterval(() => {
setFiles(prev => prev.map((f, i) =>
i === index && f.progress < 90
? { ...f, progress: f.progress + 10 }
: f
));
}, 200);
const { error } = await supabase.storage
.from('documents')
.upload(filePath, file);
clearInterval(progressInterval);
if (error) throw error;
const { data } = supabase.storage
.from('documents')
.getPublicUrl(filePath);
setFiles(prev => prev.map((f, i) =>
i === index
? { ...f, progress: 100, status: 'success', url: data.publicUrl }
: f
));
} catch (error) {
setFiles(prev => prev.map((f, i) =>
i === index
? { ...f, status: 'error', error: error.message }
: f
));
}
};
const uploadAll = async () => {
for (let i = 0; i < files.length; i++) {
if (files[i].status === 'pending') {
await uploadFile(i);
}
}
};
const removeFile = (index: number) => {
setFiles(prev => prev.filter((_, i) => i !== index));
};
return (
<div className="space-y-4">
<input
type="file"
multiple
onChange={handleFileSelect}
className="block w-full text-sm text-gray-500
file:mr-4 file:py-2 file:px-4
file:rounded-full file:border-0
file:text-sm file:font-semibold
file:bg-blue-50 file:text-blue-700
hover:file:bg-blue-100"
/>
{files.length > 0 && (
<button
onClick={uploadAll}
disabled={files.every(f => f.status !== 'pending')}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
>
Upload All
</button>
)}
<div className="space-y-2">
{files.map((fileData, index) => (
<div
key={index}
className="flex items-center gap-3 p-3 border rounded-lg"
>
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-medium">
{fileData.file.name}
</span>
<button
onClick={() => removeFile(index)}
className="text-gray-400 hover:text-gray-600"
>
<X className="w-4 h-4" />
</button>
</div>
{fileData.status === 'uploading' && (
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full transition-all"
style={{ width: `${fileData.progress}%` }}
/>
</div>
)}
{fileData.status === 'success' && (
<div className="flex items-center gap-2 text-green-600 text-sm">
<CheckCircle className="w-4 h-4" />
<span>Uploaded successfully</span>
</div>
)}
{fileData.status === 'error' && (
<div className="flex items-center gap-2 text-red-600 text-sm">
<AlertCircle className="w-4 h-4" />
<span>{fileData.error}</span>
</div>
)}
</div>
</div>
))}
</div>
</div>
);
}
Image Optimization#
1. Image Transformations#
// Get optimized image URL
const getOptimizedImageUrl = (path: string, options: {
width?: number;
height?: number;
quality?: number;
format?: 'webp' | 'avif';
}) => {
const { data } = supabase.storage
.from('images')
.getPublicUrl(path, {
transform: {
width: options.width,
height: options.height,
quality: options.quality || 80,
format: options.format || 'webp'
}
});
return data.publicUrl;
};
// Usage
const thumbnailUrl = getOptimizedImageUrl('user-photo.jpg', {
width: 200,
height: 200,
quality: 75,
format: 'webp'
});
2. Responsive Images Component#
'use client';
import Image from 'next/image';
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
interface ResponsiveImageProps {
path: string;
alt: string;
sizes?: string;
}
export default function ResponsiveImage({
path,
alt,
sizes = '100vw'
}: ResponsiveImageProps) {
const supabase = createClientComponentClient();
const getImageUrl = (width: number) => {
const { data } = supabase.storage
.from('images')
.getPublicUrl(path, {
transform: {
width,
quality: 80,
format: 'webp'
}
});
return data.publicUrl;
};
return (
<Image
src={getImageUrl(1200)}
alt={alt}
width={1200}
height={800}
sizes={sizes}
srcSet={`
${getImageUrl(400)} 400w,
${getImageUrl(800)} 800w,
${getImageUrl(1200)} 1200w,
${getImageUrl(1600)} 1600w
`}
className="w-full h-auto"
/>
);
}
Security Best Practices#
1. File Type Validation#
const ALLOWED_FILE_TYPES = {
images: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
documents: ['application/pdf', 'application/msword', 'text/plain'],
videos: ['video/mp4', 'video/webm']
};
const validateFileType = (file: File, category: keyof typeof ALLOWED_FILE_TYPES) => {
return ALLOWED_FILE_TYPES[category].includes(file.type);
};
// Usage
if (!validateFileType(file, 'images')) {
throw new Error('Invalid file type. Only images are allowed.');
}
2. File Size Limits#
const MAX_FILE_SIZE = {
avatar: 2 * 1024 * 1024, // 2MB
document: 10 * 1024 * 1024, // 10MB
video: 100 * 1024 * 1024 // 100MB
};
const validateFileSize = (file: File, category: keyof typeof MAX_FILE_SIZE) => {
return file.size <= MAX_FILE_SIZE[category];
};
3. Secure File Names#
const generateSecureFileName = (originalName: string, userId: string) => {
const ext = originalName.split('.').pop();
const timestamp = Date.now();
const random = Math.random().toString(36).substring(7);
return `${userId}/${timestamp}-${random}.${ext}`;
};
Advanced Patterns#
1. Signed URLs for Private Files#
// Generate signed URL (expires in 1 hour)
const { data, error } = await supabase.storage
.from('private-documents')
.createSignedUrl('document.pdf', 3600);
if (data) {
console.log('Signed URL:', data.signedUrl);
}
2. Batch Operations#
// Delete multiple files
const filesToDelete = ['file1.jpg', 'file2.jpg', 'file3.jpg'];
const { data, error } = await supabase.storage
.from('images')
.remove(filesToDelete);
3. Move/Copy Files#
// Move file
const { data, error } = await supabase.storage
.from('images')
.move('old-path/image.jpg', 'new-path/image.jpg');
// Copy file
const { data, error } = await supabase.storage
.from('images')
.copy('source/image.jpg', 'destination/image.jpg');
Production Checklist#
- [ ] Configure storage policies for all buckets
- [ ] Implement file type validation
- [ ] Set file size limits
- [ ] Use secure file naming
- [ ] Enable CDN caching
- [ ] Implement image optimization
- [ ] Add upload progress indicators
- [ ] Handle upload errors gracefully
- [ ] Clean up unused files periodically
- [ ] Monitor storage usage
- [ ] Implement rate limiting
- [ ] Add virus scanning for user uploads
Conclusion#
Supabase Storage provides a complete solution for file management with built-in security, CDN delivery, and image transformations. Key takeaways:
- Always implement RLS policies
- Validate file types and sizes
- Use image transformations for optimization
- Implement proper error handling
- Clean up unused files regularly
Related Guides#
- Supabase Authentication & Authorization
- Supabase Realtime Complete Guide
- Building SaaS with Next.js and Supabase
Last updated: February 19, 2026
Frequently Asked Questions
Related Guides
Supabase Realtime: Complete Guide to Building Live Applications
Master Supabase Realtime with this comprehensive guide. Learn Postgres Changes, Presence, Broadcast, and build real-time features like chat, notifications, and collaborative editing.
AI Integration for Next.js + Supabase Applications
Complete guide to integrating AI capabilities into Next.js and Supabase applications. Learn OpenAI integration, chat interfaces, vector search, RAG systems,...
Complete Guide to Building SaaS with Next.js and Supabase
Master full-stack SaaS development with Next.js 15 and Supabase. From database design to deployment, learn everything you need to build production-ready...