DEV Community

How I Fixed Supabase Storage RLS Errors That the Dashboard UI Couldn't Solve

Lior Karaev on July 06, 2026

If you've ever spent hours staring at a 400 error on a Supabase Storage upload despite having RLS policies correctly configured in the dashboard ...
Collapse
 
vollos profile image
Pon

The bucket-level vs storage.objects distinction is a good catch, the dashboard really does make it look like one layer. One thing I'd double-check on the read side though: USING (bucket_id = 'cvs') for authenticated means any signed-in user can read every CV in the bucket, including applications sent to other startups. Do job seekers get accounts too, or is authenticated effectively employers-only on RamenHire?

Collapse
 
l_build profile image
Lior Karaev

Good catch, thanks for reading closely. Right now authenticated is effectively admin-only — there's no job-seeker account system, so no applicant can currently read another applicant's CV through this policy. But you're right that the policy as written doesn't actually scope to "admin" it just scopes to "any authenticated user," so it'd become a real problem the moment job-seeker accounts (if ever) got added without someone remembering to revisit this. Appreciate you flagging it — going to leave a note in the project so future-me doesn't forget the assumption this policy is quietly relying on.

Collapse
 
vollos profile image
Pon

a note for future-you is more than most projects leave behind. while you're in that file, the write side rests on the same assumption: anon INSERT on the cvs bucket takes files from anyone holding your anon key, and nothing ties an upload to an application row. a path convention per application plus a size/MIME cap in the policy is usually enough to keep the bucket from collecting junk that never came from a real applicant.

Thread Thread
 
l_build profile image
Lior Karaev

Good follow-up, and you're right on both counts. Size and MIME type are actually already capped at the policy level (5MB, PDF/Word only) so it's not fully wide open — but the piece you're pointing at, that an upload isn't tied to any application row, is a real gap I've got open right now, not something I'd already handled. Rate limiting on the API path helps, but anon can still hit Storage directly and leave orphaned files that never correspond to a real submission. A per-application path convention is exactly the right fix — appreciate you spelling out the mechanism, not just the symptom. Going to prioritize this alongside a broader move to signed upload URLs.

Thread Thread
 
vollos profile image
Pon

Signed upload URLs are the clean end state, the anon key stops being an upload ticket entirely. Sounds like you have the map. If you want, send over the storage policy once it lands and I'll tell you what still gets through, I look at these a lot.

Thread Thread
 
l_build profile image
Lior Karaev

Appreciate it — taking you up on that. Signed upload URLs are live now (anon can no longer insert into either bucket, just verified against production). Here's the current policy state on the cvs bucket: only authenticated has SELECT and DELETE, no INSERT for any role since uploads go through createSignedUploadUrl() server-side now. company-logos follows the same pattern, plus a separate confirm step before logo_url gets set, since that one's public-facing. Let me know if anything still looks like it'd get through.

Collapse
 
publiflow profile image
PubliFlow

Supabase Storage RLS can be incredibly tricky, especially when the dashboard policies don't translate correctly to the actual bucket access rules or when dealing with nested folder paths. I've found that explicitly defining the auth.uid() checks in the bucket policies rather than just relying on the default UI toggles saves so much debugging time. If you want to skip the RLS headache entirely when starting a new project, we built a Next.js and Supabase boilerplate at publiflow.vip that comes with pre-configured, battle-tested storage policies and auth flows out of the box.

Collapse
 
frank_signorini profile image
Frank

How did you handle row level security rules for storage uploads, did you use a specific policy or function to bypass the 400 error? I'd love to swap ideas on this, been struggling with similar issues.

Collapse
 
l_build profile image
Lior Karaev

Thanks, Frank! Short version: I moved from client-side USING (auth.uid() = ...) checks to explicit role + bucket_id conditions in the policy itself, and tested every policy directly against the Storage API rather than trusting the dashboard UI's simulator (that's actually what the whole post was about — the dashboard UI gave false confidence a couple of times). Happy to swap notes if you're working through something specific — what's the shape of your setup?

Collapse
 
vollos profile image
Pon

A second DELETE policy hiding under a different name, surviving a DROP IF EXISTS, is exactly the kind of thing only pg_policies ever admits to. Green migration logs lie with a straight face. Enjoyed this thread, and good luck with the employer outreach.

Collapse
 
publiflow profile image
PubliFlow

Dealing with Supabase Storage 400 errors is a painful rite of passage, especially when the dashboard gives you false confidence about your policies. I usually find the issue stems from the auth.uid() function not matching the exact user context during the upload request, or missing bucket-level policies that silently override row-level ones. If you are building a SaaS and want to avoid wrestling with these edge cases from scratch, I built a boilerplate at publivip.vip that handles these storage and auth configurations out of the box. Have you found any specific debugging tricks for when the Supabase logs just show a generic 400 without the underlying RLS violation details?