Skip to main content
Explore storage examples covering file uploads, downloads, image transformations, resumable uploads, and access control.

Basic File Upload

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(url, key)

// Upload file from File object
async function uploadFile(file: File) {
  const { data, error } = await supabase.storage
    .from('uploads')
    .upload(`public/${file.name}`, file, {
      cacheControl: '3600',
      upsert: false,
    })

  if (error) throw error
  return data
}

// Upload file from ArrayBuffer or Blob
async function uploadBlob(blob: Blob, fileName: string) {
  const { data, error } = await supabase.storage
    .from('uploads')
    .upload(fileName, blob)

  if (error) throw error
  return data
}

// Upload with custom metadata
async function uploadWithMetadata(file: File) {
  const { data, error } = await supabase.storage
    .from('uploads')
    .upload(file.name, file, {
      cacheControl: '3600',
      upsert: false,
      metadata: {
        title: 'My Document',
        author: 'John Doe',
      },
    })

  if (error) throw error
  return data
}

Download Files

// Download file
async function downloadFile(path: string) {
  const { data, error } = await supabase.storage
    .from('uploads')
    .download(path)

  if (error) throw error
  
  // Create a blob URL
  const url = URL.createObjectURL(data)
  return url
}

// Trigger download in browser
async function downloadAndSave(path: string, fileName: string) {
  const { data, error } = await supabase.storage
    .from('uploads')
    .download(path)

  if (error) throw error

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

Public URLs

// Get public URL for a file
function getPublicUrl(path: string) {
  const { data } = supabase.storage
    .from('uploads')
    .getPublicUrl(path)

  return data.publicUrl
}

// Get public URL with transformation
function getTransformedUrl(path: string) {
  const { data } = supabase.storage
    .from('uploads')
    .getPublicUrl(path, {
      transform: {
        width: 500,
        height: 500,
        resize: 'cover',
      },
    })

  return data.publicUrl
}

Signed URLs (Private Files)

// Create signed URL that expires
async function createSignedUrl(path: string, expiresIn: number = 60) {
  const { data, error } = await supabase.storage
    .from('private-uploads')
    .createSignedUrl(path, expiresIn)

  if (error) throw error
  return data.signedUrl
}

// Create multiple signed URLs
async function createSignedUrls(paths: string[]) {
  const { data, error } = await supabase.storage
    .from('private-uploads')
    .createSignedUrls(paths, 60)

  if (error) throw error
  return data
}

List Files

// List all files in a bucket
async function listFiles(folder: string = '') {
  const { data, error } = await supabase.storage
    .from('uploads')
    .list(folder, {
      limit: 100,
      offset: 0,
      sortBy: { column: 'name', order: 'asc' },
    })

  if (error) throw error
  return data
}

// List files with search
async function searchFiles(searchTerm: string) {
  const { data, error } = await supabase.storage
    .from('uploads')
    .list('', {
      search: searchTerm,
    })

  if (error) throw error
  return data
}

Delete Files

// Delete single file
async function deleteFile(path: string) {
  const { error } = await supabase.storage
    .from('uploads')
    .remove([path])

  if (error) throw error
}

// Delete multiple files
async function deleteFiles(paths: string[]) {
  const { error } = await supabase.storage
    .from('uploads')
    .remove(paths)

  if (error) throw error
}

Move and Copy Files

// Move file
async function moveFile(fromPath: string, toPath: string) {
  const { error } = await supabase.storage
    .from('uploads')
    .move(fromPath, toPath)

  if (error) throw error
}

// Copy file
async function copyFile(fromPath: string, toPath: string) {
  const { error } = await supabase.storage
    .from('uploads')
    .copy(fromPath, toPath)

  if (error) throw error
}

Image Transformations

// Resize image
function getResizedImage(path: string, width: number, height: number) {
  const { data } = supabase.storage
    .from('images')
    .getPublicUrl(path, {
      transform: {
        width,
        height,
        resize: 'cover', // 'cover', 'contain', 'fill'
      },
    })

  return data.publicUrl
}

// Convert format
function getConvertedImage(path: string, format: 'webp' | 'avif' | 'jpeg') {
  const { data } = supabase.storage
    .from('images')
    .getPublicUrl(path, {
      transform: {
        format,
        quality: 80,
      },
    })

  return data.publicUrl
}

// Multiple transformations
function getOptimizedImage(path: string) {
  const { data } = supabase.storage
    .from('images')
    .getPublicUrl(path, {
      transform: {
        width: 800,
        height: 600,
        resize: 'cover',
        format: 'webp',
        quality: 80,
      },
    })

  return data.publicUrl
}

Resumable Upload with Uppy

Install Dependencies

npm install @uppy/core @uppy/dashboard @uppy/tus @uppy/react

React Component

import { Dashboard } from '@uppy/react'
import Uppy from '@uppy/core'
import Tus from '@uppy/tus'
import { useState, useEffect } from 'react'
import { supabase } from '@/lib/supabase'
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 ${supabase.auth.session()?.access_token}`,
      },
      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}
    />
  )
}

Storage Policies

Public Bucket Policies

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

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

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

Private Bucket Policies

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

create policy "Users can upload to their own folder"
  on storage.objects for insert
  with check (
    bucket_id = 'private-uploads'
    and auth.uid()::text = (storage.foldername(name))[1]
  );

Avatar Upload Component

import { useState } from 'react'
import { supabase } from '@/lib/supabase'
import Image from 'next/image'

export default function AvatarUpload({ userId }: { userId: string }) {
  const [avatarUrl, setAvatarUrl] = useState<string | null>(null)
  const [uploading, setUploading] = useState(false)

  const uploadAvatar = 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 = `${userId}.${fileExt}`
      const filePath = `avatars/${fileName}`

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

      if (uploadError) throw uploadError

      // Get public URL
      const { data } = supabase.storage
        .from('avatars')
        .getPublicUrl(filePath)

      setAvatarUrl(data.publicUrl)
    } catch (error: any) {
      alert(error.message)
    } finally {
      setUploading(false)
    }
  }

  return (
    <div className="flex flex-col items-center gap-4">
      {avatarUrl ? (
        <Image
          src={avatarUrl}
          alt="Avatar"
          width={150}
          height={150}
          className="rounded-full"
        />
      ) : (
        <div className="w-[150px] h-[150px] rounded-full bg-gray-200" />
      )}
      
      <label className="cursor-pointer px-4 py-2 bg-blue-500 text-white rounded">
        {uploading ? 'Uploading...' : 'Upload Avatar'}
        <input
          type="file"
          accept="image/*"
          onChange={uploadAvatar}
          disabled={uploading}
          className="hidden"
        />
      </label>
    </div>
  )
}

File Upload with Progress

import { useState } from 'react'
import { supabase } from '@/lib/supabase'

export default function UploadWithProgress() {
  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 = `uploads/${Date.now()}_${file.name}`

      // Upload with progress tracking
      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: any) {
      alert(error.message)
    } 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}% uploaded
          </p>
        </div>
      )}
    </div>
  )
}

Multi-File Upload

import { useState } from 'react'
import { supabase } from '@/lib/supabase'

interface UploadedFile {
  name: string
  url: string
}

export default function MultiFileUpload() {
  const [uploading, setUploading] = useState(false)
  const [files, setFiles] = useState<UploadedFile[]>([])

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

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

      const uploadedFiles: UploadedFile[] = []

      for (const file of Array.from(event.target.files)) {
        const filePath = `uploads/${Date.now()}_${file.name}`

        const { error } = await supabase.storage
          .from('uploads')
          .upload(filePath, file)

        if (error) throw error

        const { data } = supabase.storage
          .from('uploads')
          .getPublicUrl(filePath)

        uploadedFiles.push({
          name: file.name,
          url: data.publicUrl,
        })
      }

      setFiles(uploadedFiles)
      alert('All files uploaded successfully!')
    } catch (error: any) {
      alert(error.message)
    } finally {
      setUploading(false)
    }
  }

  return (
    <div className="space-y-4">
      <input
        type="file"
        multiple
        onChange={uploadFiles}
        disabled={uploading}
        className="block w-full"
      />
      
      {files.length > 0 && (
        <ul className="space-y-2">
          {files.map((file, index) => (
            <li key={index} className="flex items-center gap-2">
              <span>{file.name}</span>
              <a
                href={file.url}
                target="_blank"
                rel="noopener noreferrer"
                className="text-blue-500"
              >
                View
              </a>
            </li>
          ))}
        </ul>
      )}
    </div>
  )
}

Example Apps

User Management

Avatar uploads with profiles

Resumable Upload (Uppy)

Large file uploads with Uppy

Image Gallery

Image transformations

File Manager

Complete file management

Next Steps

Storage Docs

Learn more about Storage

File Upload Guide

Detailed upload tutorial

Image Transformations

Master image manipulation

Access Control

Secure your files