Supabase Storage Bucket Policies are the gatekeepers for your files, and they’re far more powerful than just "public" or "private."

Let’s see them in action. Imagine you have a Supabase project with a public bucket for user avatars. You want to allow anyone to read an avatar, but only the authenticated user who owns the avatar to update it.

Here’s how you’d set that up in the Supabase UI:

  1. Navigate to Storage: In your Supabase dashboard, go to the "Storage" section.

  2. Select Your Bucket: Click on the public bucket.

  3. Go to Policies: Click on the "Policies" tab.

  4. Create a New Policy: Click "New Policy."

  5. Name the Policy: Give it a descriptive name, like users_can_read_own_avatar.

  6. Select Operations: For this policy, check SELECT (read) and UPDATE.

  7. Define the Rule: This is where the magic happens. You’ll use SQL syntax.

    For SELECT:

    -- Allow anyone to read any file
    true
    

    For UPDATE:

    -- Allow only the authenticated user who owns the file to update it
    -- Assuming the file path is like 'avatars/user_id/avatar.png'
    -- And the user's ID is available in `auth.uid()`
    -- And the file path contains the user's ID
    -- We need to extract the user_id from the file path.
    -- Let's assume a common path structure like: 'avatars/{user_id}/...'
    -- We can use `split_part` or regex to extract the user_id.
    -- For simplicity, let's assume the user_id is the first segment after 'avatars/'
    -- This might need adjustment based on your exact file naming convention.
    
    -- Example: path is 'avatars/a1b2c3d4-e5f6-7890-1234-567890abcdef/my_avatar.jpg'
    -- We want to check if `auth.uid()` matches the user_id in the path.
    
    -- Using split_part (simpler if path is consistent):
    -- split_part(file_path, '/', 2) extracts the second part after splitting by '/'.
    -- If path is 'avatars/user_id/filename.ext', then split_part(file_path, '/', 2) will be 'user_id'.
    split_part(file_path, '/', 2) = auth.uid()
    
    -- OR using regex (more robust for variations):
    -- This regex checks if the file_path starts with 'avatars/', followed by a UUID, then '/'.
    -- And it captures that UUID.
    -- file_path ~ '^avatars/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/'
    -- AND we want to ensure the captured UUID matches auth.uid()
    -- This is more complex to write directly in the policy editor for capture and comparison.
    -- The `split_part` approach is often sufficient and easier for common structures.
    
    -- Let's stick with the split_part for clarity in this example:
    split_part(file_path, '/', 2) = auth.uid()
    
  8. Save Policies: Click "Save."

Now, any authenticated user can fetch any avatar. But only the user whose ID matches the user_id part of the file path can upload or overwrite their own avatar.

The core problem Supabase Storage Policies solve is granular, row-level security applied to files. Unlike traditional file storage where you might have ACLs on directories or individual files, Supabase leverages PostgreSQL’s powerful Row Level Security (RLS) system to define who can perform what operation on which file, based on user authentication and data within your database tables.

The file_path is a special pseudo-column available within the policy editor. It represents the unique path to the file within the bucket. When you define a policy, you’re essentially writing SQL WHERE clauses that are evaluated against this file_path and your authenticated user’s identity (auth.uid()).

Consider a more complex scenario: a private document repository for a specific project. You might have a projects table and a project_files table. A project_files entry would have a project_id and a file_url (pointing to your Supabase Storage).

To ensure only project members can access files:

  1. Link Tables: Ensure your project_files table has a project_id foreign key to your projects table.

  2. Membership Table: You’d likely have a project_members table linking user_id to project_id.

  3. Storage Policy: In your Supabase Storage UI, for the bucket holding these files, create a policy.

    • Name: project_members_can_access_files
    • Operations: SELECT
    • Rule:
      -- Check if the user is a member of the project associated with the file
      EXISTS (
          SELECT 1
          FROM project_members pm
          JOIN project_files pf ON pf.project_id = pm.project_id
          WHERE pf.file_url = file_path -- Link the file path to the record in project_files
          AND pm.user_id = auth.uid()   -- Ensure the current user is in project_members
      )
      
    • Important Note: For this to work, your project_files table must have a mechanism to link a file_url to a project_id. This is often done by ensuring the file_url column in project_files directly matches the file_path in storage. If your file_path is structured like projects/{project_id}/filename.ext, you can simplify the WHERE clause:
      -- If file_path is like 'projects/some_project_id/some_file.pdf'
      -- Extract project_id from the file_path
      split_part(file_path, '/', 2) IN (
          SELECT project_id
          FROM project_members
          WHERE user_id = auth.uid()
      )
      

What most people miss is how file_path can be dynamically parsed within policies to infer relationships. It’s not just a string; it’s a data point you can operate on. For example, if your file paths follow a strict convention like users/{auth.uid()}/documents/{document_id}.pdf, you can create policies that are extremely precise:

  • Allow SELECT on users/{auth.uid()}/... for any authenticated user.
  • Allow INSERT on users/{auth.uid()}/... only if a corresponding record exists in your documents table where owner_id = auth.uid() and document_id can be extracted from the filename.
  • Deny DELETE on any file not matching users/{auth.uid()}/... or if a related record in your documents table has an is_archived flag set to true.

The next hurdle is understanding how to manage signed URLs for temporary, time-limited access to private files.

Want structured learning?

Take the full Supabase course →