File Uploads
Handle file uploads with storage service.
Note: This is mock/placeholder content for demonstration purposes.
Enable users to upload and manage files using the storage service.
Warning: This files stack assumes self-service user deletion is disabled by default. If you enable
BETTER_AUTH_ENABLE_SELF_SERVICE_ACCOUNT_DELETION, review file ownership and sharing cleanup carefully. User-owned files,createdByIdreferences, and related grants may require explicit cleanup or reassignment to avoid unexpected ownership or access edge cases.
Setup
Configure Storage Buckets
Configure storage buckets in your storage service configuration:
// config/storage.config.ts
export const storageConfig = {
buckets: {
avatars: {
public: true,
maxSize: 5 * 1024 * 1024, // 5MB
},
documents: {
public: false,
maxSize: 10 * 1024 * 1024, // 10MB
},
},
};
Set Storage Policies
Configure access policies in your storage service:
// Allow users to upload their own avatars
export const storagePolicies = {
avatars: {
upload: (userId: string, path: string) => {
return path.startsWith(`${userId}/`);
},
view: (userId: string, path: string) => {
return path.startsWith(`${userId}/`);
},
delete: (userId: string, path: string) => {
return path.startsWith(`${userId}/`);
},
},
};
Upload Component
Basic File Upload
'use client';
import { useState } from 'react';
import { uploadFileAction } from '../_lib/actions';
export function FileUpload() {
const [uploading, setUploading] = useState(false);
const [file, setFile] = useState<File | null>(null);
const handleUpload = async () => {
if (!file) return;
setUploading(true);
const formData = new FormData();
formData.append('file', file);
const result = await uploadFileAction(formData);
if (result.success) {
toast.success('File uploaded successfully');
}
setUploading(false);
};
return (
<div>
<input
type="file"
onChange={(e) => setFile(e.files?.[0] || null)}
accept="image/*"
/>
<button
onClick={handleUpload}
disabled={!file || uploading}
>
{uploading ? 'Uploading...' : 'Upload'}
</button>
</div>
);
}
Server Action
'use server';
import { enhanceAction } from '@kit/next/actions';
import { uploadFile } from '@kit/storage/server';
export const uploadFileAction = enhanceAction(
async (formData: FormData, user) => {
const file = formData.get('file') as File;
if (!file) {
throw new Error('No file provided');
}
const fileExt = file.name.split('.').pop();
const fileName = `${user.id}/${Date.now()}.${fileExt}`;
const { url, path } = await uploadFile({
bucket: 'avatars',
fileName,
file,
options: {
cacheControl: '3600',
upsert: false,
},
});
return {
success: true,
url,
path,
};
},
{ auth: true }
);
Drag and Drop Upload
Use the built-in ImageUploader component from @kit/ui/image-uploader for drag-and-drop image uploads:
'use client';
import { useState } from 'react';
import { ImageUploader } from '@kit/ui/image-uploader';
export function DragDropUpload() {
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const handleFileChange = (file: File | null) => {
if (file) {
setPreviewUrl(URL.createObjectURL(file));
// Upload file...
} else {
setPreviewUrl(null);
}
};
return (
<ImageUploader
value={previewUrl}
onValueChange={handleFileChange}
onError={(error) => {
if (error === 'size') toast.error('File too large');
if (error === 'type') toast.error('Invalid file type');
}}
>
<span>Click to upload or drag and drop</span>
</ImageUploader>
);
}
For more advanced uploads with progress tracking, use the useUppyImageUpload hook:
'use client';
import { useUppyImageUpload } from '@kit/ui/hooks/use-uppy-image-upload';
export function UppyUpload() {
const { addFile, upload, isUploading, previewUrl, error } = useUppyImageUpload(
{
type: 'user',
onUploadSuccess: (url) => console.log('Uploaded:', url),
onUploadError: (err) => console.error('Failed:', err),
},
);
return (
<div>
<input
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) addFile(file);
}}
/>
{previewUrl && <img src={previewUrl} alt="Preview" />}
<button onClick={() => upload()} disabled={isUploading}>
{isUploading ? 'Uploading...' : 'Upload'}
</button>
{error && <p className="text-red-500">{error}</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 uploadFileAction = enhanceAction(
async (formData: FormData, user) => {
const file = formData.get('file') as 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.type)) {
throw new Error('Invalid file type');
}
// Validate dimensions for images
if (file.type.startsWith('image/')) {
const dimensions = await getImageDimensions(file);
if (dimensions.width > 4000 || dimensions.height > 4000) {
throw new Error('Image dimensions too large');
}
}
// Continue with upload...
},
{ auth: true }
);
Image Optimization
Resize on Upload
import sharp from 'sharp';
export const uploadAvatarAction = enhanceAction(
async (formData: FormData, user) => {
const file = formData.get('file') as File;
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`;
await uploadFile({
bucket: 'avatars',
fileName,
file: resized,
options: {
contentType: 'image/jpeg',
upsert: true,
},
});
return { success: true };
},
{ auth: true }
);
Progress Tracking
'use client';
import { useState } from 'react';
export function UploadWithProgress() {
const [progress, setProgress] = useState(0);
const handleUpload = async (file: File) => {
await uploadFile({
bucket: 'documents',
fileName: `uploads/${file.name}`,
file,
onProgress: (progressEvent) => {
const percent = (progressEvent.loaded / progressEvent.total) * 100;
setProgress(Math.round(percent));
},
});
};
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
import { getPublicUrl } from '@kit/storage/server';
const url = getPublicUrl({
bucket: 'avatars',
fileName: 'user-id/avatar.jpg',
});
console.log(url);
Download Private File
import { downloadFile } from '@kit/storage/server';
const file = await downloadFile({
bucket: 'documents',
fileName: 'private-file.pdf',
});
if (file) {
const url = URL.createObjectURL(file);
const a = document.createElement('a');
a.href = url;
a.download = 'file.pdf';
a.click();
}
Generate Signed URL
import { getSignedUrl } from '@kit/storage/server';
const url = await getSignedUrl({
bucket: 'documents',
fileName: 'private-file.pdf',
expiresIn: 3600, // 1 hour
});
console.log(url);
Deleting Files
import { deleteFile } from '@kit/storage/server';
export const deleteFileAction = enhanceAction(
async (data, user) => {
await deleteFile({
bucket: 'avatars',
fileName: data.path,
});
return { success: true };
},
{
schema: z.object({
path: z.string(),
}),
auth: 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