Checklist
Supabase Storage: bucket policies
Short answer. A public bucket lets anyone with the file URL read it. Uploads, deletes, moves, and copies still go through policies on storage.objects. A private bucket, the default, checks those policies for downloads too. Hand out private files with a signed URL that expires. The service role key bypasses the policies, so it stays on the server.
Updated 14 Nov 2026
This is a practical checklist, not a guarantee. It is not a penetration test, a compliance audit, or proof that an app is safe to launch.
Table policies and file policies are different objects. Row policies for public tables are in the Supabase row level security checklist and Supabase RLS policy examples. This page is only Storage. Official pages: Storage access control, bucket fundamentals, and serving assets.
Public and private buckets
Supabase's bucket fundamentals say a bucket is private by default. On a private bucket, every operation, including download, is subject to row level security on storage.objects. The ways to download are a request that sends the user's JWT, which the policy evaluates, or a signed URL from createSignedUrl.
A public bucket bypasses access control for retrieving and serving files. Anyone who has the asset URL can read the file. The public URL shape, from the serving page, is:
https://[project_id].supabase.co/storage/v1/object/public/[bucket]/[asset-name]getPublicUrl builds that address. It does not check a policy. Uploading, deleting, moving, and copying in a public bucket still go through access control. Supabase's examples for a public bucket are profile pictures, public media, and blog content. It also says public buckets are cached differently and are more performant than private ones.
Use a public bucket only for files you mean anyone to fetch. A private document, an export, or a file that belongs to one account belongs in a private bucket.
Policies on storage.objects
Supabase says Storage does not allow uploads until you add policies on storage.objects. An upload needs INSERT. Overwriting a file with upsert also needs SELECT and UPDATE. Write a policy per operation, the same way you would for a table.
storage.foldername(name) returns the folders as an array. For public/subfolder/avatar.png the first element is public. Supabase's user-folder example compares that element to auth.jwt() ->> 'sub', which is the user id. auth.uid() is that same id. The select example compares owner_id to that id. owner_id is text, so the comparison is text, not a uuid.
create policy "Users upload into their own folder"
on storage.objects
for insert
to authenticated
with check (
bucket_id = 'files'
and (storage.foldername(name))[1] = (select auth.jwt() ->> 'sub')
);
create policy "Users update files in their own folder"
on storage.objects
for update
to authenticated
using (
bucket_id = 'files'
and (storage.foldername(name))[1] = (select auth.jwt() ->> 'sub')
)
with check (
bucket_id = 'files'
and (storage.foldername(name))[1] = (select auth.jwt() ->> 'sub')
);
create policy "Users delete files in their own folder"
on storage.objects
for delete
to authenticated
using (
bucket_id = 'files'
and (storage.foldername(name))[1] = (select auth.jwt() ->> 'sub')
);
create policy "Users read their own objects"
on storage.objects
for select
to authenticated
using (
bucket_id = 'files'
and (select auth.jwt() ->> 'sub') = owner_id
);When to use the folder check: each person should only write under a prefix that is their user id, for example files/<their id>/report.pdf.
When to use an authenticated read of one bucket, without a folder check: every signed-in user may read every object in that bucket, and signed-out visitors may not. That is the bucket_id filter from Supabase's insert example, applied to select:
create policy "Signed-in users read this bucket"
on storage.objects
for select
to authenticated
using ( bucket_id = 'files' );to authenticated stops the policy applying to the anon role. A policy with no to clause applies to every role. The access-control page also shows a public read that is not a public bucket: a select policy limited with storage.allow_any_operation to the get operations. Without that filter, Supabase says users can list the bucket. That extra policy is not needed when the bucket itself is public, because those files are already reachable by URL.
Common mistake: leaving the starter policy in place. The access-control page begins with for insert with check (true) and says to modify it. true allows the insert for every role the policy applies to. Another mistake is a select policy that checks bucket_id only, on a bucket of private files, so every signed-in user can read every object.
SELECT on storage.objects covers more than one Storage action. Listing objects and downloading an authenticated object can share that privilege. If those should differ, Supabase's helper docs say to use storage.allow_only_operation() or storage.allow_any_operation().
Signed URLs
For a private bucket, the serving page says to sign a time-limited URL on the server, for example in an Edge Function, or to download with the user's JWT. The JavaScript reference says createSignedUrl(path, expiresIn) expires in seconds: 60 is one minute. The serving page's example passes 3600.
const { data, error } = await supabase.storage
.from("files")
.createSignedUrl("user-id/report.pdf", 60)The reference also says this call needs select on storage.objects. A browser client using the anon key can sign a URL only when that user's policy allows the select. Sign from the server when the file should not be listed to the browser first. Do not put the service role key in that browser client. A service key bypasses Storage RLS. Supabase says not to share it. If it is already in the bundle or the chat, use Service role key leaked? What to do. The anon key is the other key. What it may do is Is the Supabase anon key safe to expose?.
You cannot revoke one signed URL. The serving page says to delete, move, or rename the object, or rotate the project's URL signing key and then revoke the previous key. Signed URLs use a Storage signing key, not the Auth JWT signing key, so rotating the Auth key does not invalidate them. The Smart CDN page says a cached response for that exact URL can keep being served after the token time has passed. Deleting the object invalidates cached copies. Token expiry alone does not.
Choose a short expiry for a one-time download. Reuse the same signed URL only when many requests should share it. If the file has no per-user restriction, Supabase says a public bucket caches better and removes the need to sign.
File type and size
Bucket fundamentals say max file size and allowed content types are set on the bucket. The creating buckets page shows allowedMimeTypes, for example ['image/*'], and fileSizeLimit, for example '1MB'. An upload that does not match is rejected.
There is also a project-wide limit. The file limits page says the global maximum is 50 MB on the Free plan and up to 500 GB on Pro, Team, and higher, and that a bucket limit cannot be higher than the global one. Set the global limit to the largest file you accept, and a smaller limit on each bucket.
Checklist
- Decide public or private per bucket. Private is the default. Public means anyone with the URL can read.
- Do not leave
with check (true)onstorage.objects. - For per-user files, check
bucket_idand the first folder fromstorage.foldername(name)against the caller id. Add insert, update, delete, and select. Upsert needs select and update as well as insert. - Sign private downloads on the server. Pass
expiresInin seconds. Do not sign with the service role key in the browser. - Set allowed MIME types and a file size limit on the bucket, under the project global limit.
- Then run a free passive check on the public URL. It does not read your Storage policies. The worksheets for this pass are the checklist pack. Supabase is the directory entry.
FAQ
Does a public bucket still check who can upload?
Yes. Supabase says a public bucket bypasses access control for retrieving and serving files. Uploading, deleting, moving, and copying still go through policies on storage.objects. A private bucket checks downloads as well.
Can I revoke one signed URL?
Not by itself. Supabase says to delete, move, or rename the object, or to rotate the Storage URL signing key and revoke the previous key. A CDN cache of that exact URL can outlive the token. Deleting the object is what drops the cached copies.
Is the service role key a way to let the browser upload?
No. The service key bypasses Storage RLS. Supabase says not to share it. The browser should use the publishable or anon key, and the policies should allow that user to write only their own folder.
Sources
Checked 14 Nov 2026.
Informational only. This page describes checks and vendor documentation. It is not a security audit, a penetration test, or a certification, and it is not a guarantee.
Newsletter
A short weekly note on new tools and security checklists. No spam, unsubscribe anytime.