← Back to Tutorials Chapter 9

Storage: Insert and File Upload

Supabase Storage Overview

Supabase Storage provides S3-compatible object storage backed by your Postgres database. Every file you upload is stored as an object in a storage bucket, with metadata tracked in the storage.objects table. Storage integrates directly with Supabase's built-in CDN, so files are served from edge locations worldwide. This means you get fast uploads and downloads without managing any additional infrastructure — just create a bucket, upload files, and retrieve them with public or signed URLs.

Storage Buckets

Buckets are the top-level containers for your files. You create them through the Supabase Dashboard or programmatically via the API. Each bucket has a visibility setting: public buckets allow anyone with the URL to read files, while private buckets require signed URLs or RLS policies for access. Choose public for assets like avatars and product images that should be visible to everyone. Use private for user-specific documents, receipts, or any file with access restrictions.

Bucket names must follow S3 conventions — lowercase alphanumeric characters and hyphens only, no underscores or spaces. A good naming pattern uses the purpose and environment: avatars, product-images-prod, documents-dev. Create a bucket through the dashboard by navigating to Storage → New Bucket, or use the API:

const { data, error } = await supabase
  .storage
  .createBucket('avatars', { public: true });

File Upload

Uploading a file uses the upload method on a storage bucket reference. The path is relative to the bucket root and should include a filename:

const { data, error } = await supabase
  .storage
  .from('avatars')
  .upload('user-123/profile.png', file, {
    upsert: false,
    contentType: 'image/png'
  });

The second argument is a File, Blob, or ArrayBuffer. The upsert option controls whether an existing file at the same path is overwritten — set it to true if you want to replace files. Always set the contentType explicitly; otherwise Supabase tries to infer it, which can result in incorrect MIME types and broken downloads.

To upload from a browser file input:

<input type="file" id="fileInput" accept="image/*" />

<script>
  const fileInput = document.getElementById('fileInput');
  fileInput.addEventListener('change', async (e) => {
    const file = e.target.files[0];
    if (!file) return;

    const filePath = `${user.id}/${file.name}`;
    const { data, error } = await supabase
      .storage
      .from('avatars')
      .upload(filePath, file, { upsert: true });

    if (error) {
      console.error('Upload failed:', error.message);
    } else {
      console.log('Uploaded:', data.path);
    }
  });
</script>

Listing Files

To list files in a folder within a bucket, use the list method. It returns an array of file objects with name, id, size, and metadata:

const { data, error } = await supabase
  .storage
  .from('avatars')
  .list('user-123', {
    limit: 100,
    offset: 0,
    sortBy: { column: 'name', order: 'asc' }
  });

data.forEach(file => {
  console.log(file.name, file.metadata.size);
});

The list method does not recurse into subfolders. Each "folder" is a logical prefix — objects in subpaths appear as separate entries.

Public URLs vs Signed URLs

For public buckets, use getPublicUrl to generate a URL anyone can use to view or download a file:

const { data } = supabase
  .storage
  .from('avatars')
  .getPublicUrl('user-123/profile.png');

// data.publicUrl -> "https://<project>.supabase.co/storage/v1/object/public/avatars/user-123/profile.png"

For private buckets or files that need time-limited access, use createSignedUrl. The URL expires after the specified number of seconds:

const { data, error } = await supabase
  .storage
  .from('documents')
  .createSignedUrl('receipt-2024.pdf', 60); // expires in 60 seconds

// data.signedUrl -> temporary signed URL

Signed URLs are perfect for serving private user documents like invoices, medical records, or any content that should not be publicly accessible. For public assets like profile pictures or blog images, stick with getPublicUrl — it requires no network request and works instantly.

File Types and Size Limits

Supabase accepts any file type, but the default maximum file size is 5 GB per upload when using the S3 protocol directly. When uploading through the Supabase JavaScript client library, the effective limit depends on your network and the multipart upload configuration. For very large files, consider using the S3-compatible API with multipart upload chunks. Common web file types — images, PDFs, audio, video — all work without special configuration.

Note: The 5 GB file size limit applies to individual uploads. For larger files, use multipart upload through the S3 API or split the file before uploading. Also, the browser's own memory constraints may limit how large a file you can upload via <input type="file"> — for files over 100 MB, consider chunked uploads.

Image Optimization with Transformations

Supabase Storage supports server-side image transformations. You can resize, crop, and convert images on the fly by adding query parameters to the public or signed URL:

const { data } = supabase
  .storage
  .from('avatars')
  .getPublicUrl('user-123/profile.png', {
    transform: {
      width: 200,
      height: 200,
      quality: 80,
      format: 'webp'
    }
  });

Supported parameters include width, height, quality (0–100), resize (cover, contain, fill), and format (origin, avif, webp, jpeg). Transformations happen on the CDN edge, so the original file stays untouched. Use this to serve thumbnails, responsive images, or to convert to modern formats like WebP for smaller file sizes and faster page loads. Combine transformations with signed URLs to deliver optimized private images securely.

Common Pitfalls

Exercise: Build a simple file upload page that lets users upload a profile picture and displays it. Create a public bucket called avatars, use a file input and Supabase JS SDK to upload the selected file, then retrieve and display it using getPublicUrl with a 150x150 transformation. Handle upload progress and error states in the UI.