Skip to main content
This guide covers everything you need to know about handling file uploads with Supabase Storage, from basic uploads to advanced features like resumable uploads and image transformations.

What You’ll Learn

  • Upload files to Supabase Storage
  • Download and display files
  • Implement resumable uploads for large files
  • Apply image transformations
  • Set up access control policies
  • Handle file metadata

Prerequisites

  • A Supabase project
  • Basic understanding of JavaScript/TypeScript
  • Supabase client library installed

Storage Basics

Supabase Storage organizes files in buckets. Each bucket can have:
  • Public or private access
  • Custom file size limits
  • Allowed MIME types
  • Storage policies for access control
1

Create a Storage Bucket

In your Supabase Dashboard:
  1. Navigate to Storage
  2. Click New bucket
  3. Configure your bucket:
    • Name: uploads (or your preferred name)
    • Public bucket: Toggle on for public access
    • File size limit: Set as needed (e.g., 50MB)
    • Allowed MIME types: Leave empty or specify types
  4. Click Create bucket
2

Set Up Storage Policies

Create policies to control access:
-- Allow authenticated users to upload files
create policy "Authenticated users can upload files"
  on storage.objects for insert
  with check (
    bucket_id = 'uploads' 
    and auth.role() = 'authenticated'
  );

-- Allow public read access
create policy "Public files are publicly accessible"
  on storage.objects for select
  using (bucket_id = 'uploads');

-- Allow users to update their own files
create policy "Users can update their own files"
  on storage.objects for update
  using (
    bucket_id = 'uploads'
    and auth.uid()::text = (storage.foldername(name))[1]
  );

-- Allow users to delete their own files
create policy "Users can delete their own files"
  on storage.objects for delete
  using (
    bucket_id = 'uploads'
    and auth.uid()::text = (storage.foldername(name))[1]
  );
3

Basic File Upload

Create a simple file upload component:
import { createClient } from '@supabase/supabase-js'
import { useState } from 'react'

export default function FileUpload() {
  const supabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
  
  const [uploading, setUploading] = useState(false)
  const [fileUrl, setFileUrl] = useState<string | null>(null)

  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('uploads')
        .upload(filePath, file)

      if (uploadError) {
        throw uploadError
      }

      // Get public URL
      const { data } = supabase.storage
        .from('uploads')
        .getPublicUrl(filePath)
      
      setFileUrl(data.publicUrl)
      alert('File uploaded successfully!')
    } catch (error) {
      alert('Error uploading file!')
      console.error(error)
    } finally {
      setUploading(false)
    }
  }

  return (
    <div className="space-y-4">
      <div>
        <label 
          htmlFor="file-upload" 
          className="px-4 py-2 bg-blue-500 text-white rounded cursor-pointer"
        >
          {uploading ? 'Uploading...' : 'Upload File'}
        </label>
        <input
          id="file-upload"
          type="file"
          className="hidden"
          onChange={uploadFile}
          disabled={uploading}
        />
      </div>
      
      {fileUrl && (
        <div>
          <p>File uploaded successfully!</p>
          <a 
            href={fileUrl} 
            target="_blank" 
            rel="noopener noreferrer"
            className="text-blue-500 underline"
          >
            View file
          </a>
        </div>
      )}
    </div>
  )
}
4

Upload with Progress Tracking

Track upload progress for better UX:
import { useState } from 'react'
import { createClient } from '@supabase/supabase-js'

export default function UploadWithProgress() {
  const supabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
  
  const [progress, setProgress] = useState(0)
  const [uploading, setUploading] = useState(false)

  const uploadFile = async (event: React.ChangeEvent<HTMLInputElement>) => {
    try {
      setUploading(true)
      setProgress(0)

      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 filePath = `${Date.now()}_${file.name}`

      // Create XMLHttpRequest for progress tracking
      const xhr = new XMLHttpRequest()

      xhr.upload.addEventListener('progress', (e) => {
        if (e.lengthComputable) {
          const percentComplete = (e.loaded / e.total) * 100
          setProgress(percentComplete)
        }
      })

      // Use Supabase storage upload
      const { error } = await supabase.storage
        .from('uploads')
        .upload(filePath, file, {
          cacheControl: '3600',
          upsert: false
        })

      if (error) throw error

      setProgress(100)
      alert('Upload complete!')
    } catch (error) {
      alert('Error uploading file!')
      console.error(error)
    } finally {
      setUploading(false)
    }
  }

  return (
    <div className="space-y-4">
      <input
        type="file"
        onChange={uploadFile}
        disabled={uploading}
        className="block w-full"
      />
      
      {uploading && (
        <div>
          <div className="w-full bg-gray-200 rounded-full h-2.5">
            <div 
              className="bg-blue-600 h-2.5 rounded-full transition-all"
              style={{ width: `${progress}%` }}
            />
          </div>
          <p className="text-sm text-gray-600 mt-2">
            {progress.toFixed(0)}% uploaded
          </p>
        </div>
      )}
    </div>
  )
}
5

Resumable Upload with Uppy

For large files, use resumable uploads with Uppy:Install dependencies:
npm install @uppy/core @uppy/dashboard @uppy/tus @uppy/react
Create the component:
import { Dashboard } from '@uppy/react'
import Uppy from '@uppy/core'
import Tus from '@uppy/tus'
import { useState, useEffect } from 'react'
import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css'

export default function ResumableUpload({ userId }: { userId: string }) {
  const [uppy] = useState(() =>
    new Uppy({
      restrictions: {
        maxFileSize: 1000000000, // 1GB
        maxNumberOfFiles: 5,
      },
      autoProceed: false,
    })
  )

  useEffect(() => {
    uppy.use(Tus, {
      endpoint: `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/upload/resumable`,
      headers: {
        authorization: `Bearer ${process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY}`,
      },
      chunkSize: 6 * 1024 * 1024, // 6MB chunks
      allowedMetaFields: [
        'bucketName',
        'objectName',
        'contentType',
        'cacheControl',
      ],
    })

    uppy.on('file-added', (file) => {
      file.meta = {
        ...file.meta,
        bucketName: 'uploads',
        objectName: `${userId}/${file.name}`,
        contentType: file.type,
      }
    })

    uppy.on('complete', (result) => {
      console.log('Upload complete:', result)
    })

    return () => uppy.close()
  }, [uppy, userId])

  return (
    <Dashboard
      uppy={uppy}
      proudlyDisplayPoweredByUppy={false}
      height={450}
    />
  )
}
6

Image Upload with Transformations

Upload and transform images:
import { createClient } from '@supabase/supabase-js'
import { useState } from 'react'
import Image from 'next/image'

export default function ImageUpload() {
  const supabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
  
  const [imageUrl, setImageUrl] = useState<string | null>(null)
  const [uploading, setUploading] = useState(false)

  const uploadImage = async (event: React.ChangeEvent<HTMLInputElement>) => {
    try {
      setUploading(true)

      if (!event.target.files || event.target.files.length === 0) {
        throw new Error('You must select an image to upload.')
      }

      const file = event.target.files[0]
      const fileExt = file.name.split('.').pop()
      const fileName = `${Math.random()}.${fileExt}`
      const filePath = `images/${fileName}`

      // Upload the file
      const { error: uploadError } = await supabase.storage
        .from('uploads')
        .upload(filePath, file)

      if (uploadError) throw uploadError

      // Get transformed image URL
      const { data } = supabase.storage
        .from('uploads')
        .getPublicUrl(filePath, {
          transform: {
            width: 500,
            height: 500,
            resize: 'cover',
          },
        })
      
      setImageUrl(data.publicUrl)
    } catch (error) {
      alert('Error uploading image!')
      console.error(error)
    } finally {
      setUploading(false)
    }
  }

  return (
    <div className="space-y-4">
      <div>
        <label 
          htmlFor="image-upload" 
          className="px-4 py-2 bg-blue-500 text-white rounded cursor-pointer"
        >
          {uploading ? 'Uploading...' : 'Upload Image'}
        </label>
        <input
          id="image-upload"
          type="file"
          accept="image/*"
          className="hidden"
          onChange={uploadImage}
          disabled={uploading}
        />
      </div>
      
      {imageUrl && (
        <div className="relative w-full h-64">
          <Image
            src={imageUrl}
            alt="Uploaded image"
            fill
            className="object-cover rounded"
          />
        </div>
      )}
    </div>
  )
}
7

Download Files

Download files from storage:
const downloadFile = async (path: string) => {
  try {
    const { data, error } = await supabase.storage
      .from('uploads')
      .download(path)

    if (error) throw error

    // Create a blob URL and trigger download
    const url = URL.createObjectURL(data)
    const link = document.createElement('a')
    link.href = url
    link.download = path.split('/').pop() || 'download'
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
    URL.revokeObjectURL(url)
  } catch (error) {
    console.error('Error downloading file:', error)
  }
}
8

List Files in a Bucket

List and display uploaded files:
import { useEffect, useState } from 'react'
import { createClient } from '@supabase/supabase-js'

export default function FileList() {
  const supabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
  
  const [files, setFiles] = useState<any[]>([])

  useEffect(() => {
    loadFiles()
  }, [])

  const loadFiles = async () => {
    const { data, error } = await supabase.storage
      .from('uploads')
      .list('', {
        limit: 100,
        offset: 0,
        sortBy: { column: 'created_at', order: 'desc' },
      })

    if (error) {
      console.error('Error loading files:', error)
    } else {
      setFiles(data)
    }
  }

  const deleteFile = async (fileName: string) => {
    const { error } = await supabase.storage
      .from('uploads')
      .remove([fileName])

    if (error) {
      console.error('Error deleting file:', error)
    } else {
      loadFiles() // Refresh the list
    }
  }

  return (
    <div className="space-y-2">
      <h2 className="text-xl font-bold">Uploaded Files</h2>
      <ul className="space-y-2">
        {files.map((file) => (
          <li key={file.name} className="flex items-center justify-between p-2 border rounded">
            <span>{file.name}</span>
            <button
              onClick={() => deleteFile(file.name)}
              className="px-3 py-1 bg-red-500 text-white rounded text-sm"
            >
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  )
}

Advanced Features

Image Transformations

Supabase Storage supports on-the-fly image transformations:
const { data } = supabase.storage
  .from('uploads')
  .getPublicUrl('image.jpg', {
    transform: {
      width: 800,
      height: 600,
      resize: 'cover', // 'cover', 'contain', 'fill'
      format: 'webp',  // 'webp', 'avif', 'jpeg', 'png'
      quality: 80,
    },
  })

File Metadata

Store custom metadata with files:
const { error } = await supabase.storage
  .from('uploads')
  .upload('file.pdf', file, {
    cacheControl: '3600',
    upsert: false,
    metadata: {
      title: 'My Document',
      author: 'John Doe',
    },
  })

Signed URLs

Create temporary signed URLs for private files:
const { data, error } = await supabase.storage
  .from('private-bucket')
  .createSignedUrl('file.pdf', 60) // Expires in 60 seconds

if (data) {
  console.log('Signed URL:', data.signedUrl)
}

Best Practices

File Naming

Use unique filenames to avoid conflicts. Consider using UUIDs or timestamps.

File Size Limits

Set appropriate file size limits in your bucket settings and validate on the client.

MIME Type Validation

Restrict allowed file types to prevent malicious uploads.

Folder Organization

Organize files in folders using paths like userId/filename.ext.

Error Handling

Always handle errors gracefully and provide user feedback.

Next Steps

Storage Policies

Learn about advanced access control

Image Transformations

Master image manipulation

User Management

Build complete user profiles

Storage Examples

Explore more storage examples