DEV Community

A silent bug: signing Supabase Storage URLs in the wrong bucket

This week we fixed a bug in Brifora (an AI design studio for small businesses, built with Next.js and Supabase) that never threw a single error.

The symptom. In "My works", cross-stitch patterns showed up without a download link. Logos, pictures and videos were fine.

The cause. We store files in several Supabase Storage buckets, because each bucket has its own allowed MIME types and size limit: images in generations, videos in videos, PDF patterns in patterns. The history page signed every path in generations, with one exception for .mp4. A PDF path signed in the wrong bucket doesn't fail — createSignedUrl simply finds nothing and returns no URL. The UI then shows an empty slot, which looks exactly like "the file was never saved".

The fix. One shared function decides the bucket from the file extension, and every place that signs or deletes files uses it:

export function bucketForPath(path: string): StorageBucket {
  if (path.endsWith('.mp4')) return 'videos'
  if (path.endsWith('.pdf')) return 'patterns'
  return 'generations'
}
Enter fullscreen mode Exit fullscreen mode

Then group the paths by bucket and sign each group in one batch call — one request per bucket, not one per file.

Lessons:

  1. If a rule ("which bucket?") lives in two files, it will drift. Put it in one place.
  2. Extension is a safe signal only if you set it when storing the file — not the provider.
  3. A missing URL should be visible in the UI as an error, not as an empty frame.
  4. Watch what the fix exposes: once PDFs had links, they would have landed in an <img> tag. We added a separate "PDF" tile.

Brifora: https://brifora.app — built by Weblevo (weblevo.com).

Top comments (2)

Collapse
 
vladtkdev profile image
Vlad Tkachenko •

createSignedUrl does fail here. The server looks the object up before it signs anything (findObject, in storage's object.ts), so a .pdf path asked for in generations is a 404 not_found and supabase-js hands back { data: null, error }. The empty slot was that error being dropped, so lesson 3 is a call-site fix.

One to watch when you batch it: createSignedUrls answers 200 with the outer error null and reports per path instead, as data[i].error, with signedUrl null under a type that says string.

Also, the bucket is a fact about the row you wrote, not about the filename. Is it on the row already, or only the path?

Collapse
 
patchandship profile image
PatchAndShip •

nice writeup. once the bucket picker is centralized, one extra improvement to consider: show the storage error in the ui when signing fails, not an empty slot. then a wrong-bucket path looks like a failed sign instead of a missing upload.