File storage with Supabase Storage
Intermediate

File storage with Supabase Storage

In this codelab you'll learn what file storage is, create Supabase Storage buckets, upload and download files, generate public and signed URLs, and secure everything with Row Level Security policies.

Before you start

This codelab assumes you've completed:

What you'll use

25–35 min

1What is file storage?

Most web apps need to store files — profile photos, uploaded documents, product images, or downloadable content. These files are too large and too different from regular data to store in a database.

Instead, apps use file storage services — cloud-based systems designed specifically for storing and serving files. They handle the heavy lifting: storage, delivery via CDN, access control, and more.

Files apps commonly store

  • User profile pictures and avatars
  • Product images for an online store
  • PDF documents, invoices, and reports
  • Audio and video files for media apps

💡 Think of it this way

A database is like a filing cabinet — great for organized records like names and numbers. File storage is like a warehouse — built for larger, varied items like photos, videos, and documents that don't fit neatly in a drawer.

2What is Supabase Storage?

Supabase Storage is a file storage service built right into Supabase. It organizes files into buckets — think of them as top-level folders. Each bucket can hold unlimited files and subfolders.

  • Buckets Organize files into named containers — one for avatars, one for documents, etc.
  • Public and private access Public buckets give every file a shareable URL. Private buckets require authentication.
  • CDN delivery Files are served through a global CDN, so they load fast anywhere in the world.
  • RLS policies Use Row Level Security to control who can upload, download, and delete files.

Supabase Storage uses the same authentication system as the rest of Supabase, so you can reuse your existing user accounts and permissions.

💡 Tip: If you haven't set up a Supabase project yet, start with our Getting started with Supabase codelab

3Create a bucket

Buckets are the top-level containers for your files. Let's create one:

  1. Go to your Supabase dashboard at supabase.com/dashboard
  2. Click Storage in the left sidebar.
  3. Click New bucket.
  4. Enter a name (e.g. avatars), choose Public or Private, and click Create bucket.

Public vs. private buckets

  • Public Anyone with the URL can access files. Great for profile pictures, product images, and public assets.
  • Private Files require authentication or a signed URL. Use for sensitive documents, invoices, and user uploads.

You can also create buckets with SQL:

Document contents
-- You can also create a bucket with SQL
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true);

4Upload files

Install the Supabase JavaScript client if you haven't already:

Terminal
npm install @supabase/supabase-js

Here's how to upload a file from your app:

Document contents
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  'https://YOUR_PROJECT.supabase.co',
  'YOUR_ANON_KEY'
);

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

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

You can also upload from an HTML file input:

Document contents
// Upload from an HTML file input
const fileInput = document.getElementById('file-input');
fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (file) await uploadFile(file);
});

⚠️ File size: The free plan allows files up to 50 MB. For larger files, you'll need a paid plan.

5Get public URLs

For public buckets, every file gets a permanent URL you can use in your app:

Document contents
// Get the public URL for a file
const { data } = supabase.storage
  .from('avatars')
  .getPublicUrl('public/profile.jpg');

console.log(data.publicUrl);
// https://YOUR_PROJECT.supabase.co/storage/v1/object/public/avatars/public/profile.jpg

For private buckets, you'll need a signed URL — a temporary link that expires after a set time:

Document contents
// For private buckets, generate a temporary signed URL
const { data, error } = await supabase.storage
  .from('documents')
  .createSignedUrl('reports/q1-report.pdf', 3600); // 1 hour

console.log(data.signedUrl);

Signed URLs are perfect for protecting sensitive files like invoices or private documents while still allowing temporary access.

6List and delete files

You can list all files in a bucket or folder:

Document contents
// List files in a folder
const { data, error } = await supabase.storage
  .from('avatars')
  .list('public', {
    limit: 100,
    offset: 0,
    sortBy: { column: 'created_at', order: 'desc' },
  });

console.log(data);
// [{ name: 'profile.jpg', ... }, { name: 'banner.png', ... }]

Deleting files is just as easy:

Document contents
// Delete a single file
const { error } = await supabase.storage
  .from('avatars')
  .remove(['public/old-photo.jpg']);

// Delete multiple files at once
const { error: bulkError } = await supabase.storage
  .from('avatars')
  .remove([
    'public/photo1.jpg',
    'public/photo2.jpg',
    'public/photo3.jpg',
  ]);

The remove() method accepts an array of file paths, so you can delete one file or many in a single call.

7Secure with RLS policies

By default, storage buckets have no access policies — meaning nobody can read or write files through the API. You need to create RLS (Row Level Security) policies to define who can do what.

Here are common policies you'll want to set up:

Document contents
-- Allow anyone to view files in the avatars bucket
CREATE POLICY "Public read access"
  ON storage.objects FOR SELECT
  USING (bucket_id = 'avatars');

-- Allow authenticated users to upload to their own folder
CREATE POLICY "Users can upload own files"
  ON storage.objects FOR INSERT
  WITH CHECK (
    bucket_id = 'avatars'
    AND auth.uid()::text = (storage.foldername(name))[1]
  );

-- Allow users to delete their own files
CREATE POLICY "Users can delete own files"
  ON storage.objects FOR DELETE
  USING (
    bucket_id = 'avatars'
    AND auth.uid()::text = (storage.foldername(name))[1]
  );

A common pattern is to name each user's folder after their user ID. This makes it easy to write policies that restrict access to the file owner.

💡 Tip: RLS works the same way for storage as it does for database tables. Learn more in our Getting started with Supabase codelab

8Build a file upload component

Let's put it all together by building a React component that uploads an image and displays it:

Document contents
// FileUpload.jsx — a React file upload component
import { useState } from 'react';
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_ANON_KEY
);

function FileUpload() {
  const [uploading, setUploading] = useState(false);
  const [fileUrl, setFileUrl] = useState(null);

  const handleUpload = async (e) => {
    const file = e.target.files[0];
    if (!file) return;

    setUploading(true);

    const filePath = `uploads/${Date.now()}-${file.name}`;
    const { error } = await supabase.storage
      .from('avatars')
      .upload(filePath, file);

    if (error) {
      alert('Upload failed: ' + error.message);
    } else {
      const { data } = supabase.storage
        .from('avatars')
        .getPublicUrl(filePath);

      setFileUrl(data.publicUrl);
    }

    setUploading(false);
  };

  return (
    <div>
      <input
        type="file"
        accept="image/*"
        onChange={handleUpload}
        disabled={uploading}
      />
      {uploading && <p>Uploading...</p>}
      {fileUrl && (
        <div>
          <p>Uploaded!</p>
          <img src={fileUrl} alt="Uploaded file" width={200} />
        </div>
      )}
    </div>
  );
}

export default FileUpload;

This component handles the complete flow: selecting a file, uploading it to Supabase Storage, and displaying the uploaded image with its public URL.

💡 New to React?

If you haven't built a React app yet, check out our Build a React app with Vite codelab

9What's next

You've learned file storage with Supabase! Here are some great next steps:

🗄️ Database integration

Store file metadata alongside your data — Getting started with Supabase

⚛️ Build a React app

Create a frontend to showcase your uploads — Build a React app with Vite

🔗 Full-stack wiring

Connect your frontend to a custom backend — Connect frontend to backend

Deploy your app

Put your app online — Deploy web apps with Vercel

🆓 Free plan storage limits

  • 1 GB total file storage
  • 50 MB maximum file size
  • 2 GB bandwidth per month

📖 Lesson: See how file storage fits into web application architecture in our Elements of a web application lesson.

🎉

You did it!

You've learned file storage with Supabase — creating buckets, uploading and downloading files, generating URLs, securing with RLS, and building a file upload component. You're ready to add file storage to your apps!

Back to codelabs

How was this codelab?

Let us know what you thought — suggestions, issues, or anything else.

File storage with Supabase Storage