Supabase Storage images not loading

Profile photos or uploads show a broken image? A public URL only works on a public bucket. A private bucket needs a signed URL or the user's JWT.

Reviewed October 17, 2026

What it means in plain English

The file can be in the bucket while the <img> tag still fails. Supabase Storage has two access models. A public bucket lets anyone with the file URL read it. A private bucket does not. Buckets are private by default. getPublicUrl only builds the public URL. It does not make a private bucket public.

Likely causes

  • The app uses a public URL for a bucket that is still private. Supabase says assets in a non-public bucket are not accessible via a public URL.
  • The bucket is public, but the path in the URL does not match the object name, including folders.
  • The bucket is private and the page calls download() without the user's JWT. Supabase says a private download needs an authorization header, and the SELECT policy on storage.objects decides if that user can read the file.
  • The upload itself was refused. That is a different failure. See Storage upload blocked by RLS.

How to fix it, step by step

  1. In the dashboard open Storage and check the bucket. Public means anyone with the URL can retrieve the file. Upload, delete, move, and copy are still checked. Private means every read is checked too.
  2. For images that should be public, such as profile photos or blog media, set the bucket to public. The URL Supabase documents is:
https://[project_id].supabase.co/storage/v1/object/public/[bucket]/[asset-name]

getPublicUrl builds that URL:

const { data } = supabase.storage.from('avatars').getPublicUrl('filePath.jpg');
  1. For files that must stay private, do not put the public URL in an <img> tag. Either download with the signed-in client, or create a time-limited signed URL. Supabase's example expires in 3600 seconds:
const { data, error } = await supabase.storage
  .from('bucket')
  .createSignedUrl('private-document.pdf', 3600);

Supabase says to sign that URL on the server, for example in an Edge Function, when you do not want the browser to hold a path it should not list. A signed URL stays valid until it expires. It is signed with a storage key, not the Auth JWT, so rotating the Auth signing key does not revoke it.

  1. On a private bucket, add a SELECT policy on storage.objects for the role that should see the file. A public bucket skips that check for reads.
  2. Paste this to the AI tool:
Images from Supabase Storage are broken. If this bucket should be public, mark it public and render the URL from storage.from(bucket).getPublicUrl(path). If it must stay private, stop using /storage/v1/object/public/ and use createSignedUrl on the server, or download() with the signed-in user's session. Add a SELECT policy on storage.objects for that bucket. Do not change unrelated tables.

How to check it worked

  • Open the image URL in a private window. A public object loads. A private object does not.
  • A signed-in user sees the private image, and the signed URL stops working after the expiry you set.
  • A new upload still appears in the bucket. If the upload fails, use the Storage RLS page instead of this one.

Sources

More Supabase Storage fixes

Keep going on your own.

The guides stay free. A deep leak report is $15. Fix packs are $24, or $49 for all three. There is no done-for-you repair.

See products