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:
-
Navigate to Storage: In your Supabase dashboard, go to the "Storage" section.
-
Select Your Bucket: Click on the
publicbucket. -
Go to Policies: Click on the "Policies" tab.
-
Create a New Policy: Click "New Policy."
-
Name the Policy: Give it a descriptive name, like
users_can_read_own_avatar. -
Select Operations: For this policy, check
SELECT(read) andUPDATE. -
Define the Rule: This is where the magic happens. You’ll use SQL syntax.
For
SELECT:-- Allow anyone to read any file trueFor
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() -
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:
-
Link Tables: Ensure your
project_filestable has aproject_idforeign key to yourprojectstable. -
Membership Table: You’d likely have a
project_memberstable linkinguser_idtoproject_id. -
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_filestable must have a mechanism to link afile_urlto aproject_id. This is often done by ensuring thefile_urlcolumn inproject_filesdirectly matches thefile_pathin storage. If yourfile_pathis structured likeprojects/{project_id}/filename.ext, you can simplify theWHEREclause:-- 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() )
- Name:
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
SELECTonusers/{auth.uid()}/...for any authenticated user. - Allow
INSERTonusers/{auth.uid()}/...only if a corresponding record exists in yourdocumentstable whereowner_id = auth.uid()anddocument_idcan be extracted from the filename. - Deny
DELETEon any file not matchingusers/{auth.uid()}/...or if a related record in yourdocumentstable has anis_archivedflag set totrue.
The next hurdle is understanding how to manage signed URLs for temporary, time-limited access to private files.