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, createdById references, 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

  1. Validate on both sides - Client and server
  2. Limit file sizes - Prevent abuse
  3. Sanitize filenames - Remove special characters
  4. Use unique names - Prevent collisions
  5. Optimize images - Resize before upload
  6. Set storage policies - Control access
  7. Monitor usage - Track storage costs
  8. Clean up unused files - Regular maintenance
  9. Use CDN - For public files
  10. Implement virus scanning - For user uploads