Updated · 2 min read
the short answer
Is my Supabase storage bucket public?
Check the Public flag on each bucket in Storage, or run select id, public from storage.buckets. A public bucket serves every file to anyone who has or guesses the URL, with no login and no policy check. Keep user files (IDs, invoices, private photos) in a private bucket, give each user a folder named after their user ID, add storage policies that compare that folder to auth.uid(), and show files through short-lived signed URLs.
Public bucket or private bucket
AI tools often make a bucket public because a private one returns an error the first time the image does not load. The app works, and every uploaded passport scan is now one URL away from anyone.
- Public: files are served from /storage/v1/object/public/<bucket>/<path> to anyone. Fine for logos, product images and avatars that are meant to be seen.
- Private: every read goes through your storage policies. Use it for anything a user uploads that is not meant for everyone.
Check and fix your buckets
Run this in the SQL editor, or open Storage in the dashboard and look for the Public label.
select id, public from storage.buckets;
-- make a bucket private
update storage.buckets set public = false where id = 'documents';Give each user their own folder
Upload to a path that starts with the user ID, then write policies on storage.objects that only allow that user into that folder. A policy that only checks bucket_id lets every signed-in user list and download everyone's files.
create policy "Users upload to own folder" on storage.objects
for insert to authenticated
with check (bucket_id = 'documents' and (storage.foldername(name))[1] = auth.uid()::text);
create policy "Users read own files" on storage.objects
for select to authenticated
using (bucket_id = 'documents' and (storage.foldername(name))[1] = auth.uid()::text);
create policy "Users delete own files" on storage.objects
for delete to authenticated
using (bucket_id = 'documents' and (storage.foldername(name))[1] = auth.uid()::text);Show files with signed URLs
A signed URL works for a limited time and only after your policies allowed the request. Upload to the user's folder and create the link when the page needs it.
const path = user.id + '/' + file.name;
await supabase.storage.from('documents').upload(path, file);
const { data } = await supabase.storage.from('documents').createSignedUrl(path, 60); // valid for 60 secondsUploads need limits too
Set a maximum file size and the allowed file types on the bucket (in the bucket settings), so nobody can fill your storage or host files on your project. Never let users pick another user's folder in the upload path.
Questions
Are Supabase storage files private by default?
New buckets are private unless you tick Public. But many templates and AI-generated apps create public buckets, so check each one.
Does RLS on my tables protect storage?
No. Storage has its own policies on the storage.objects table. Table policies do not cover files, and a public bucket skips policies for reads entirely.
Are random file names enough to keep files private?
No. URLs end up in logs, browser history, shared screenshots and the network tab. Use a private bucket and signed URLs for anything personal.
Can I keep avatars public and documents private?
Yes. Use two buckets: a public one for images meant to be seen and a private one with per-user policies for everything else.