Skip to main content
Supabase Storage provides multiple methods to download and access files, whether they’re public or private.

Download Methods

Download as Blob

Download files as Blob objects for client-side processing:
const { data, error } = await supabase
  .storage
  .from('avatars')
  .download('public/avatar1.png')

if (error) {
  console.error('Download error:', error.message)
} else {
  // data is a Blob
  console.log('File size:', data.size)
  console.log('MIME type:', data.type)
  
  // Create object URL for display
  const url = URL.createObjectURL(data)
  document.getElementById('image').src = url
}

Public URLs

For public buckets, get direct URLs:
const { data } = supabase
  .storage
  .from('avatars')
  .getPublicUrl('public/avatar1.png')

console.log('Public URL:', data.publicUrl)

// Use in HTML
// <img src="{data.publicUrl}" alt="Avatar" />

Signed URLs

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

if (error) {
  console.error('Error creating signed URL:', error.message)
} else {
  console.log('Signed URL:', data.signedUrl)
  console.log('Expires at:', new Date(data.expiresAt))
}

Download Component

Complete React component for downloading files:
React Component
import { useState } from 'react'
import { supabase } from './supabaseClient'

export default function FileDownload({ fileName, bucketName }) {
  const [downloading, setDownloading] = useState(false)

  const downloadFile = async () => {
    setDownloading(true)

    try {
      const { data, error } = await supabase
        .storage
        .from(bucketName)
        .download(fileName)

      if (error) throw error

      // Create blob URL
      const url = URL.createObjectURL(data)

      // Create temporary link and trigger download
      const link = document.createElement('a')
      link.href = url
      link.download = fileName
      document.body.appendChild(link)
      link.click()
      document.body.removeChild(link)

      // Clean up
      URL.revokeObjectURL(url)
      
    } catch (error) {
      console.error('Download failed:', error)
      alert('Failed to download file')
    } finally {
      setDownloading(false)
    }
  }

  return (
    <button onClick={downloadFile} disabled={downloading}>
      {downloading ? 'Downloading...' : 'Download File'}
    </button>
  )
}

Image Display

Display images from Storage:
Image Component
import { useState, useEffect } from 'react'
import { supabase } from './supabaseClient'

export default function StorageImage({ path, bucketName = 'avatars' }) {
  const [imageUrl, setImageUrl] = useState(null)

  useEffect(() => {
    async function getImage() {
      // For public buckets
      const { data } = supabase
        .storage
        .from(bucketName)
        .getPublicUrl(path)
      
      setImageUrl(data.publicUrl)
    }

    if (path) {
      getImage()
    }
  }, [path, bucketName])

  if (!imageUrl) {
    return <div>Loading...</div>
  }

  return (
    <img
      src={imageUrl}
      alt="Storage image"
      style={{ maxWidth: '100%' }}
    />
  )
}

Private Image Display

For private buckets, use signed URLs:
Private Image Component
import { useState, useEffect } from 'react'
import { supabase } from './supabaseClient'

export default function PrivateImage({ path, bucketName = 'private' }) {
  const [imageUrl, setImageUrl] = useState(null)

  useEffect(() => {
    async function getSignedUrl() {
      const { data, error } = await supabase
        .storage
        .from(bucketName)
        .createSignedUrl(path, 3600) // 1 hour expiry

      if (error) {
        console.error('Error getting signed URL:', error)
        return
      }

      setImageUrl(data.signedUrl)
    }

    if (path) {
      getSignedUrl()
    }
  }, [path, bucketName])

  if (!imageUrl) {
    return <div>Loading...</div>
  }

  return <img src={imageUrl} alt="Private image" />
}

List Files

Retrieve list of files in a bucket:
const { data, error } = await supabase
  .storage
  .from('documents')
  .list('user-123', {
    limit: 100,
    offset: 0,
    sortBy: { column: 'name', order: 'asc' },
  })

if (error) {
  console.error('Error listing files:', error)
} else {
  data.forEach(file => {
    console.log('Name:', file.name)
    console.log('Size:', file.metadata.size)
    console.log('Updated:', file.updated_at)
  })
}

File Browser Component

File Browser
import { useState, useEffect } from 'react'
import { supabase } from './supabaseClient'

export default function FileBrowser({ bucketName, folder = '' }) {
  const [files, setFiles] = useState([])
  const [loading, setLoading] = useState(true)

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

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

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

  async function downloadFile(fileName) {
    const filePath = folder ? `${folder}/${fileName}` : fileName
    
    const { data, error } = await supabase
      .storage
      .from(bucketName)
      .download(filePath)

    if (error) {
      alert('Download failed: ' + error.message)
      return
    }

    const url = URL.createObjectURL(data)
    const link = document.createElement('a')
    link.href = url
    link.download = fileName
    link.click()
    URL.revokeObjectURL(url)
  }

  if (loading) {
    return <div>Loading files...</div>
  }

  return (
    <div>
      <h2>Files in {bucketName}/{folder || 'root'}</h2>
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Size</th>
            <th>Updated</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          {files.map((file) => (
            <tr key={file.name}>
              <td>{file.name}</td>
              <td>{(file.metadata?.size / 1024).toFixed(2)} KB</td>
              <td>{new Date(file.updated_at).toLocaleDateString()}</td>
              <td>
                <button onClick={() => downloadFile(file.name)}>
                  Download
                </button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

Search Files

Search for files with specific patterns:
const { data, error } = await supabase
  .storage
  .from('documents')
  .list('reports', {
    search: '2024',
    limit: 50,
    sortBy: { column: 'name', order: 'asc' },
  })

const pdf Files = data.filter(file => file.name.endsWith('.pdf'))
console.log('PDF files:', pdfFiles)

Download Multiple Files

Download several files at once:
async function downloadMultipleFiles(bucketName, filePaths) {
  const downloadPromises = filePaths.map(async (path) => {
    const { data, error } = await supabase
      .storage
      .from(bucketName)
      .download(path)

    if (error) {
      console.error(`Error downloading ${path}:`, error)
      return null
    }

    return { path, blob: data }
  })

  const results = await Promise.all(downloadPromises)
  return results.filter(Boolean)
}

// Usage
const files = await downloadMultipleFiles('documents', [
  'file1.pdf',
  'file2.pdf',
  'file3.pdf'
])

console.log(`Downloaded ${files.length} files`)

Stream Large Files

For very large files, stream the download:
async function streamDownload(bucketName, filePath) {
  const { data } = supabase
    .storage
    .from(bucketName)
    .getPublicUrl(filePath)

  const response = await fetch(data.publicUrl)
  const reader = response.body.getReader()
  
  let receivedLength = 0
  const chunks = []

  while(true) {
    const { done, value } = await reader.read()
    
    if (done) break
    
    chunks.push(value)
    receivedLength += value.length
    
    console.log(`Received ${receivedLength} bytes`)
  }

  const blob = new Blob(chunks)
  return blob
}

Download as Data URL

Convert downloaded file to base64 data URL:
async function getFileAsDataUrl(bucketName, filePath) {
  const { data, error } = await supabase
    .storage
    .from(bucketName)
    .download(filePath)

  if (error) throw error

  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.onloadend = () => resolve(reader.result)
    reader.onerror = reject
    reader.readAsDataURL(data)
  })
}

// Usage
const dataUrl = await getFileAsDataUrl('avatars', 'avatar.png')
console.log(dataUrl) // data:image/png;base64,...

CDN Caching

Public URLs are automatically cached by Supabase’s CDN:
// URLs are cached based on the path
const { data } = supabase
  .storage
  .from('images')
  .getPublicUrl('logo.png')

// To bust cache, add a query parameter
const urlWithCacheBust = `${data.publicUrl}?t=${Date.now()}`

Download with Progress

Track download progress:
async function downloadWithProgress(bucketName, filePath, onProgress) {
  const { data: publicUrlData } = supabase
    .storage
    .from(bucketName)
    .getPublicUrl(filePath)

  const response = await fetch(publicUrlData.publicUrl)
  const contentLength = response.headers.get('content-length')
  
  const reader = response.body.getReader()
  let receivedLength = 0
  const chunks = []

  while (true) {
    const { done, value } = await reader.read()

    if (done) break

    chunks.push(value)
    receivedLength += value.length

    const progress = (receivedLength / contentLength) * 100
    onProgress(progress)
  }

  return new Blob(chunks)
}

// Usage
const blob = await downloadWithProgress(
  'videos',
  'large-video.mp4',
  (progress) => console.log(`${progress.toFixed(2)}%`)
)

File Metadata

Get file information without downloading:
const { data, error } = await supabase
  .storage
  .from('documents')
  .list('', {
    limit: 1,
    search: 'report.pdf'
  })

if (data && data.length > 0) {
  const file = data[0]
  console.log('Name:', file.name)
  console.log('Size:', file.metadata.size)
  console.log('MIME type:', file.metadata.mimetype)
  console.log('Last modified:', file.metadata.lastModified)
}

Error Handling

Common download errors:
ErrorDescriptionSolution
Object not foundFile doesn’t existVerify file path
Bucket not foundBucket doesn’t existCheck bucket name
UnauthorizedNo access to private fileCheck RLS policies or use signed URL
Resource not foundInvalid pathVerify full path including folders

Best Practices

Use Public URLs

For public files, use getPublicUrl for better performance

Cache Signed URLs

Cache signed URLs client-side to reduce API calls

Clean Up Object URLs

Always revoke object URLs with URL.revokeObjectURL()

Handle Large Files

Use streaming or progress tracking for large downloads

Next Steps

Image Transformations

Resize and optimize images on-the-fly

Access Control

Secure files with RLS policies

Build docs developers (and LLMs) love