Demo, all content is generated
Question

Images in my app break after an hour, signed URLs from a private bucket

Solved · 386 views · asked by sunita_d · edited

Recipes app. Photos are in a private bucket. When I load the list page I call createSignedUrl(path, 3600) for each photo and store the URL in the recipes.image_url column so I don't have to do it every time. After an hour all images show broken. Should I just make the expiry a year?

What I’ve tried

Set expiry to 606024*365, it works but Cursor warned that's a security issue. Also tried making the bucket public but then anyone can list files?

Comment
Are the photos actually private? Recipe photos sound like something anyone can see anyway. hannah_reyes · edited

3 answers

Marked as helpful by the asker
hannah_reyes · edited

Store the path, never the signed URL. Signed URLs are meant to be generated at render time.

  • Save recipes.image_path = 'user-id/abc.jpg'.
  • On the page, sign them in one call: supabase.storage.from('photos').createSignedUrls(paths, 3600) returns all URLs in one request, no N+1.

On the public-bucket question: a public bucket makes files readable by anyone who has the URL. It does not let anyone list files, listing still goes through the RLS policies on storage.objects. If recipe photos aren't private anyway (they're on a public recipe page), a public bucket with random file names is the simplest option and caches better on the CDN.

Comment
They are public recipes, so public bucket it is. Didn't know listing is separate, thanks sunita_d · edited
createSignedUrls (plural) is one of those functions nobody knows exists. Good call. pawel_z · edited
sofia_gr · edited

If you keep it private: a year-long signed URL is basically a public link that you can't revoke. Keep them short and regenerate.

Comment
ximena_c · edited

Bonus of the public bucket: if you render these with next/image, signed URLs with a new token every time also defeat the image cache. Same photo, new URL, new optimization.

Comment