File Uploads
Handle file uploads with Supabase Storage.
Note: This is mock/placeholder content for demonstration purposes.
Enable users to upload and manage files using Supabase Storage.
Setup
Create Storage Bucket
-- 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);
Set Storage Policies
-- Allow 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 view their own avatars CREATE POLICY "Users can view their own avatar" ON storage.objects FOR SELECT 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] );
Upload Component
Basic File Upload
'use client';
import { useState } from 'react';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
import { createUploadedFileRecordAction } from '../_lib/actions';
export function FileUpload() {
const supabase = useSupabase();
const [uploading, setUploading] = useState(false);
const [file, setFile] = useState<File | null>(null);
const handleUpload = async () => {
if (!file) return;
setUploading(true);
const { data: auth } = await supabase.auth.getUser();
if (!auth.user) {
toast.error('You must be signed in to upload files');
setUploading(false);
return;
}
const path = `${auth.user.id}/${Date.now()}-${file.name}`;
const { data, error } = await supabase.storage
.from('avatars')
.upload(path, file);
if (error) {
toast.error('File upload failed');
setUploading(false);
return;
}
const result = await createUploadedFileRecordAction({
bucket: 'avatars',
path: data.path,
name: file.name,
contentType: file.type,
size: file.size,
});
if (result?.data?.success) {
toast.success('File uploaded successfully');
}
setUploading(false);
};
return (
<div>
<input
type="file"
onChange={(e) => setFile(e.target.files?.[0] || null)}
accept="image/*"
/>
<button
onClick={handleUpload}
disabled={!file || uploading}
>
{uploading ? 'Uploading...' : 'Upload'}
</button>
</div>
);
}
Server Action
'use server';
import { authActionClient } from '@kit/next/safe-action';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { z } from 'zod';
export const createUploadedFileRecordAction = authActionClient
.inputSchema(
z.object({
bucket: z.string().min(1),
path: z.string().min(1),
name: z.string().min(1),
contentType: z.string().min(1),
size: z.number().positive(),
}),
)
.action(async ({ parsedInput: data, ctx: { user } }) => {
const client = getSupabaseServerClient();
const { error } = await client.from('uploaded_files').insert({
user_id: user.id,
bucket: data.bucket,
path: data.path,
name: data.name,
content_type: data.contentType,
size: data.size,
});
if (error) throw error;
return {
success: true,
path: data.path,
};
});
Drag and Drop Upload
'use client';
import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
export function DragDropUpload() {
const onDrop = useCallback(async (acceptedFiles: File[]) => {
for (const file of acceptedFiles) {
await uploadFileToStorage(file);
}
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
'image/*': ['.png', '.jpg', '.jpeg', '.gif'],
},
maxSize: 5 * 1024 * 1024, // 5MB
});
return (
<div
{...getRootProps()}
className={cn(
'border-2 border-dashed rounded-lg p-8 text-center cursor-pointer',
isDragActive && 'border-primary bg-primary/10'
)}
>
<input {...getInputProps()} />
{isDragActive ? (
<p>Drop files here...</p>
) : (
<p>Drag and drop files here, or click to select</p>
)}
</div>
);
}
File Validation
Client-Side Validation
function validateFile(file: File) {
const maxSize = 5 * 1024 * 1024; // 5MB
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (file.size > maxSize) {
throw new Error('File size must be less than 5MB');
}
if (!allowedTypes.includes(file.type)) {
throw new Error('File type must be JPEG, PNG, or GIF');
}
return true;
}
Server-Side Validation
export const validateUploadedFileAction = authActionClient
.inputSchema(
z.object({
name: z.string().min(1),
contentType: z.string().min(1),
size: z.number().positive(),
width: z.number().optional(),
height: z.number().optional(),
}),
)
.action(async ({ parsedInput: file }) => {
// Validate file size
if (file.size > 5 * 1024 * 1024) {
throw new Error('File too large');
}
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!allowedTypes.includes(file.contentType)) {
throw new Error('Invalid file type');
}
// Validate dimensions for images
if (file.contentType.startsWith('image/')) {
if ((file.width ?? 0) > 4000 || (file.height ?? 0) > 4000) {
throw new Error('Image dimensions too large');
}
}
return { success: true };
});
Image Optimization
Resize on Upload
import sharp from 'sharp';
export const resizeAvatarAction = authActionClient
.inputSchema(
z.object({
sourcePath: z.string().min(1),
}),
)
.action(async ({ parsedInput: data, ctx: { user } }) => {
const client = getSupabaseServerClient();
const { data: file, error: downloadError } = await client.storage
.from('avatars')
.download(data.sourcePath);
if (downloadError) throw downloadError;
const buffer = Buffer.from(await file.arrayBuffer());
// Resize image
const resized = await sharp(buffer)
.resize(200, 200, {
fit: 'cover',
position: 'center',
})
.jpeg({ quality: 90 })
.toBuffer();
const fileName = `${user.id}/avatar.jpg`;
const { error } = await client.storage
.from('avatars')
.upload(fileName, resized, {
contentType: 'image/jpeg',
upsert: true,
});
if (error) throw error;
return { success: true };
});
Progress Tracking
'use client';
import { useState } from 'react';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
export function UploadWithProgress() {
const client = useSupabase();
const [progress, setProgress] = useState(0);
const handleUpload = async (file: File) => {
const { error } = await client.storage
.from('documents')
.upload(`uploads/${file.name}`, file, {
onUploadProgress: (progressEvent) => {
const percent = (progressEvent.loaded / progressEvent.total) * 100;
setProgress(Math.round(percent));
},
});
if (error) throw error;
};
return (
<div>
<input type="file" onChange={(e) => handleUpload(e.target.files![0])} />
{progress > 0 && (
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all"
style={{ width: `${progress}%` }}
/>
</div>
)}
</div>
);
}
Downloading Files
Get Public URL
const { data } = client.storage
.from('avatars')
.getPublicUrl('user-id/avatar.jpg');
console.log(data.publicUrl);
Download Private File
const { data, error } = await client.storage
.from('documents')
.download('private-file.pdf');
if (data) {
const url = URL.createObjectURL(data);
const a = document.createElement('a');
a.href = url;
a.download = 'file.pdf';
a.click();
}
Generate Signed URL
const { data, error } = await client.storage
.from('documents')
.createSignedUrl('private-file.pdf', 3600); // 1 hour
console.log(data.signedUrl);
Deleting Files
import { authActionClient } from '@kit/next/safe-action';
import { z } from 'zod';
export const deleteFileAction = authActionClient
.inputSchema(
z.object({
path: z.string(),
}),
)
.action(async ({ parsedInput: data }) => {
const client = getSupabaseServerClient();
const { error } = await client.storage
.from('avatars')
.remove([data.path]);
if (error) throw error;
return { success: true };
});
Best Practices
- Validate on both sides - Client and server
- Limit file sizes - Prevent abuse
- Sanitize filenames - Remove special characters
- Use unique names - Prevent collisions
- Optimize images - Resize before upload
- Set storage policies - Control access
- Monitor usage - Track storage costs
- Clean up unused files - Regular maintenance
- Use CDN - For public files
- Implement virus scanning - For user uploads