Guides / Supabase Storage public buckets

last checked oct 2026

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:

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:

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.