Public Supabase Storage buckets: are your users' files exposed?
Avatars can be public. ID scans, invoices and contracts cannot. How to check your Supabase Storage buckets and rules, and fix them without breaking uploads.
4 min read
Most apps built with Lovable, Bolt and similar tools let users upload something: a profile photo, a receipt, a contract, a passport scan for verification. On Supabase, those files go to Storage, organized in buckets.
When an AI tool creates a bucket, it often makes it public, because that makes images show up without extra work. That is fine for a logo. For anything personal, it means anyone with the link can download the file, and sometimes anyone at all can list every file in the bucket.
This guide is the Storage half of our 10-minute Supabase check. It takes about as long.
Public and private buckets
| Public bucket | Private bucket (the default) | |
|---|---|---|
| Downloading a file | Anyone with the URL, no rules checked | Only if a rule allows it, or with a signed link that expires |
| Uploading, deleting, moving | Controlled by your rules | Controlled by your rules |
| Listing the files | Controlled by your rules | Controlled by your rules |
Two things follow from this table:
- A public bucket skips your rules for downloads entirely. If file paths are predictable (
invoices/1043.pdf,ids/user-15.jpg), a stranger can guess them. - Making a bucket private doesn’t help if the rules on
storage.objectslet anyone read everything. Storage rules are ordinary Row Level Security policies on that table, and a generous one opens a private bucket just as wide.
Step 1: list your buckets
In the Supabase dashboard, open the SQL Editor and run:
select id, public, file_size_limit, allowed_mime_types
from storage.buckets
order by public desc, id;For every row with public = true, ask one question: would I be comfortable if every file in this bucket appeared on a search engine? Product images, yes. Anything a user uploaded about themselves, no.
Also look at the last two columns. Empty limits mean anyone allowed to upload can store a file of any size and any type in your project, at your cost.
Step 2: read the Storage rules
select policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'storage' and tablename = 'objects'
order by cmd, policyname;What to look for:
- A
SELECTrule foranonorpublicwhose condition is onlybucket_id = '…'. That lets anyone list and read every file in the bucket. - An
INSERTrule foranon, or forauthenticatedwithout a folder condition. That lets any user upload anywhere, including over other users’ paths if anUPDATErule is also open. - A
DELETErule that doesn’t tie the file to its owner.
The dashboard’s Security Advisor flags some of these too. Check it after any change.
Step 3: confirm from the outside
Using only your project URL and public key (under Project Settings → API Keys), try to list a bucket the way a stranger would:
curl -X POST "https://YOUR-PROJECT.supabase.co/storage/v1/object/list/BUCKET_NAME" \
-H "apikey: YOUR_PUBLIC_KEY" \
-H "Content-Type: application/json" \
-d '{"prefix": "", "limit": 20}'If this returns file names from a bucket that holds private files, the listing is open. Repeat for each bucket that holds anything personal.
How to fix it
Move personal files to a private bucket
Create a private bucket (for example documents) for anything that identifies a person or contains business data. Keep a public bucket only for files that are meant to be public.
Then change how the app shows those files. Instead of a permanent public URL, it asks Supabase for a signed URL that expires, for example after an hour. With the JavaScript client, that is createSignedUrl(path, 3600). Old public links stop working when you switch, so plan this change together with the code change, not before it.
Give each user their own folder
The common pattern is to upload each file under a folder named after the user’s ID, such as documents/8f1c…/contract.pdf, and write rules that check it:
create policy "Read own documents"
on storage.objects for select
to authenticated
using (
bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.uid()::text)
);
create policy "Upload to own folder"
on storage.objects for insert
to authenticated
with check (
bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.uid()::text)
);Add update and delete rules with the same condition if users can replace or remove their files. Uploading with “overwrite” turned on needs select, insert and update.
Keep public buckets readable but not listable
A public bucket for avatars doesn’t need a SELECT rule at all for downloads, since public URLs bypass the rules. Remove broad SELECT rules for anon on public buckets unless the app really needs to list their contents.
Set limits
Give every bucket a maximum file size and a list of allowed types (for example image/png, image/jpeg, application/pdf). It takes a minute in the bucket settings and stops your Storage from becoming free hosting for someone else.
If your app handles documents, IDs or payment records and you would like someone to look at it with you, our free Security Check reviews Storage along with database rules, keys and authentication, read-only and from the outside.
SupabaseRow Level SecurityAI-built apps
Keep reading
All articles
App security
Before you launch a Lovable app: a pre-launch security checklist
Fifteen checks to run before real users and real data arrive: database rules, keys, storage, sign-in, payments, functions and backups. Most take minutes.
4 min read

App security
OpenAI or Stripe keys in your AI-built app? Move them to a server function
Anyone can copy a key from your frontend. Which keys are safe in the browser, how to find the ones that are not, and how to move them to a Supabase Edge Function.
4 min read

App security
Found your Supabase secret key in the browser? What to do in the first hour
A service role or secret key in your frontend gives anyone full access to your database. What to do, in order: revoke, replace, check the logs, fix the cause.
5 min read