Guides / Supabase Storage public buckets
Supabase Storage bucket set to public? How to check who can read, list and upload your files
AI builders often make a Storage bucket public just to get images showing up. That's fine for avatars. It's not fine for invoices, ID photos or anything a user uploaded privately, and a loose policy on storage.objects can also let strangers list every file or upload their own.
The short answer
A public bucket skips access control for downloads. Supabase's docs say anyone who has a file's URL can fetch it. Uploading, deleting, moving and copying are still controlled by RLS policies on the storage.objects table, even in a public bucket.
So check three things:
- Which buckets are public. Anything holding private user files should be private, with access through signed URLs or the user's own session.
- Whether a public bucket can be listed. Public buckets don't need a
SELECTpolicy at all. Adding a broad one, likebucket_id = 'avatars', lets anyone list every file name in the bucket. Supabase's Security Advisor flags this aspublic_bucket_allows_listing. - Who can write. An
INSERTpolicy ofwith check (true)lets anyone upload anything to your bucket.
Buckets are private by default. If one is public, someone (or something) switched it on, often a generated migration or an "Allow public access" toggle flipped to fix a broken image.
How to check
1. List your buckets and their settings
Run this in the SQL Editor in the Supabase Dashboard:
select id, public, file_size_limit, allowed_mime_types from storage.buckets order by id;
For every row where public is true, ask whether you'd be okay with a stranger seeing any file in it. Profile pictures and blog images, probably yes. Receipts, documents, exports and private uploads, no. While you're there, an empty file_size_limit and allowed_mime_types means the bucket itself sets no upload limits.
2. Read every policy on storage.objects
select policyname, cmd, roles, qual, with_check from pg_policies where schemaname = 'storage' and tablename = 'objects' order by cmd, policyname;
Things to look for:
qualorwith_checkthat's justtrue, or only checksbucket_id, for thepublic,anonorauthenticatedroles. That covers every file in the bucket, not just the user's own.- A
SELECTpolicy on a public bucket. Downloads already work without it, so all it adds is listing. INSERT,UPDATEorDELETEpolicies with no owner or folder check. Any signed-in user, and with open signup that means anyone, could overwrite or delete other people's files.
The Security Advisor in the Dashboard runs the listing check for you. With the Supabase CLI, supabase db advisors runs the same checks from your terminal.
3. Test from outside, the way a stranger would
Only test a project you own. Put your project ref and your publishable key in environment variables first. This reads the key from your .env without printing it (change the variable name to match yours):
export SB_URL="https://your-project-ref.supabase.co" export SB_KEY="$(grep -E '^(NEXT_PUBLIC_|VITE_)?SUPABASE_(PUBLISHABLE|ANON)_KEY=' .env | head -1 | cut -d= -f2- | tr -d '"')" [ -n "$SB_KEY" ] && echo "key loaded" || echo "key not found"
Can someone with no account list the files in a bucket? This prints only the status code and how many entries came back:
curl -s -X POST "$SB_URL/storage/v1/object/list/your-bucket" \
-H "apikey: $SB_KEY" -H "Content-Type: application/json" \
-d '{"prefix":"","limit":100,"offset":0}' \
-w '\nHTTP %{http_code}\n' | python3 -c 'import sys; *body, code = sys.stdin.read().strip().split("\n"); print(code); import json; d = json.loads("\n".join(body) or "null"); print("entries:", len(d) if isinstance(d, list) else d)'
If you get entries: 0 on a bucket you know has files, listing is blocked for anonymous visitors. If you get file and folder names back, anyone can enumerate that bucket. (Supabase recommends the new sb_publishable_ key, which goes on the apikey header as shown. If you're still on the legacy anon key, add -H "Authorization: Bearer $SB_KEY" as well.)
Is a specific file readable without logging in? Public URLs follow a fixed pattern:
curl -s -o /dev/null -w '%{http_code}\n' \
"$SB_URL/storage/v1/object/public/your-bucket/path/to/file.pdf"
A 200 means anyone with that link can download it. In a private bucket the same URL fails, and that's what you want for anything sensitive.
How to fix it
Make private files private
Turn the bucket's public setting off in the Storage section of the Dashboard, or run:
update storage.buckets set public = false where id = 'documents';
Public URLs for that bucket stop working, so your app needs another way to show files. For a signed-in user viewing their own file, create a short-lived signed URL:
const { data, error } = await supabase.storage
.from('documents')
.createSignedUrl(`${userId}/invoice-123.pdf`, 60) // valid for 60 seconds
if (data) window.open(data.signedUrl)
Creating a signed URL is still checked against your storage.objects policies, so add the owner-scoped read policy below. If you'd rather never show a raw URL, download() fetches the file with the user's session instead.
Scope access to each user's own folder
Store each user's files under a folder named after their user ID, like <user-id>/invoice-123.pdf, then give each operation a policy that checks that folder. These follow the patterns in Supabase's access control docs:
-- upload only into your own folder create policy "Users upload to own folder" on storage.objects for insert to authenticated with check ( bucket_id = 'documents' and (storage.foldername(name))[1] = (select auth.jwt()->>'sub') ); -- read only your own files create policy "Users read own files" on storage.objects for select to authenticated using ( bucket_id = 'documents' and (storage.foldername(name))[1] = (select auth.jwt()->>'sub') ); -- delete only your own files create policy "Users delete own files" on storage.objects for delete to authenticated using ( bucket_id = 'documents' and (storage.foldername(name))[1] = (select auth.jwt()->>'sub') );
If your app uses upsert: true to overwrite files, Supabase's docs say it also needs SELECT and UPDATE permission, so add a matching update policy with the same check in both using and with check.
Stop a public bucket from being listed
If a bucket should stay public, like avatars, drop the broad SELECT policy. This is the fix Supabase gives for the public_bucket_allows_listing warning, and public URLs keep working afterward:
-- use the policy name from step 2 drop policy if exists "Public bucket listing" on storage.objects;
Then replace any open upload policy with one tied to the user's folder, as above, and set limits on the bucket so it can't be used to host whatever someone feels like uploading:
update storage.buckets
set file_size_limit = 2097152, -- 2 MB
allowed_mime_types = array['image/png', 'image/jpeg', 'image/webp']
where id = 'avatars';
Keep the secret key out of it
Supabase's docs say service keys bypass Storage policies entirely. If a generated app "fixed" a Storage permission error by using the service_role or sb_secret_ key in the browser, every policy above is skipped. Our guide on a Supabase service_role key exposed in client code covers how to move it and rotate it. For the table side of the same problem, see USING (true) policies.
next step
Check your live app for free
Our free Leak Checker looks at your deployed app the way any visitor's browser does and flags exposed secret keys, public source maps and missing security headers. It's a quick way to confirm no Supabase secret key shipped to the browser while you sort out your buckets.
If you'd rather have the whole app looked at, the $99 Scan + Report covers your repo and live app, including Supabase RLS and Storage policies, leaked keys and auth checks.
Sources
Every product step above was checked against these official pages. Last checked Oct 2026. Products change; if a menu moved, the docs below are the source of truth.
- Supabase: Storage buckets (public vs private access model)
- Supabase: Storage access control (storage.objects policies, upsert permissions, service key bypass)
- Supabase: Storage helper functions (storage.foldername)
- Supabase: Serving assets from Storage (public URLs, signed URLs)
- Supabase: Advisors, 0025 public_bucket_allows_listing
- Supabase: API keys (publishable key on the apikey header)
- Supabase JavaScript reference: storage list()