Supabase Storage: multipart uploads and `x-upsert` rejected with a misleading RLS error
Maintainers usually reply within 1 day
@ferhatelmas is already working on this.
Since Jul 29, 2026.
Assessment
This issue has not been assessed yet.
Description
Summary
For our project (ref: <PROJECT_REF>), Storage upload requests started failing sometime around July 3, 2026 with:
{"statusCode":"403","error":"Unauthorized","message":"new row violates row-level security policy"}
This error is misleading — the actual cause has nothing to do with the RLS policy itself. Two independent, unrelated request shapes trigger it:
- Any
multipart/form-dataupload — i.e. every call to thestorage-js/Dartstorage_clientSDK'supload()/uploadBinary()methods, since they always send multipart requests. - Any request with the
x-upsert: trueheader — even against a path that has never been used before, with no existing object to conflict with.
A raw binary body POST with no x-upsert header to a path that has never existed succeeds instantly and reliably. This was true across every bucket we tested (posts, avatars, groups, service-providers, feedbacks).
Why this is misleading
The error message and 403 status strongly suggest an RLS/auth problem. We spent significant time (verified end-to-end) ruling that out before finding the real cause:
- Confirmed the JWT was valid, non-expired, and correctly scoped (
role: authenticated,submatching the folder owner) — both by decoding it and by using the exact same token in a request that succeeded. - Confirmed the RLS policy's
storage.foldername()logic evaluates correctly against the exact object path used, via a direct SQL query againstauth.uid()/storage.foldername(). - Confirmed the SDK's client-side headers (
Authorization,apikey) were correctly populated at the moment of the request. - Only after isolating variables one at a time (multipart vs. raw body,
x-upsert: truevs.falsevs. omitted, fresh token vs. reused token) did the actual trigger become clear: it's the request shape, not the identity making the request.
Given the message says "row-level security policy," most developers (including us, initially) will spend hours auditing RLS policies, session/token handling, and auth configuration — none of which is the actual problem.
Reproduction
Replace <PROJECT_REF>, <ANON_KEY>, and <USER_ACCESS_TOKEN> with real values for a test project/user. <USER_ID> should match the authenticated user's auth.uid(), and the bucket should have a standard "users can upload/update/delete under their own auth.uid() folder" RLS policy set (INSERT/UPDATE/DELETE WITH CHECK/USING (storage.foldername(name))[1] = auth.uid()::text).
✅ Works — raw binary body, no upsert:
curl -X POST "https://<PROJECT_REF>.supabase.co/storage/v1/object/<bucket>/<USER_ID>/test.jpg" \
-H "apikey: <ANON_KEY>" \
-H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
-H "Content-Type: image/jpeg" \
--data-binary @test.jpg
# => 200, {"Key": "...", "Id": "..."}
❌ Fails — identical request, only difference is x-upsert: true added, target path has never existed:
curl -X POST "https://<PROJECT_REF>.supabase.co/storage/v1/object/<bucket>/<USER_ID>/brand-new-path.jpg" \
-H "apikey: <ANON_KEY>" \
-H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
-H "Content-Type: image/jpeg" \
-H "x-upsert: true" \
--data-binary @test.jpg
# => 400, {"statusCode":"403","error":"Unauthorized","message":"new row violates row-level security policy"}
❌ Fails — same token, multipart/form-data body (i.e. what storage_client's upload()/uploadBinary() actually send internally), no upsert:
curl -X POST "https://<PROJECT_REF>.supabase.co/storage/v1/object/<bucket>/<USER_ID>/multipart-test.jpg" \
-H "apikey: <ANON_KEY>" \
-H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
-F "[email protected];type=image/jpeg;filename=" \
-F "cacheControl=3600"
# => 400, {"statusCode":"403","error":"Unauthorized","message":"new row violates row-level security policy"}
❌ Deleting an object that genuinely exists and is genuinely owned by the requesting user also fails, with a different message:
curl -X DELETE "https://<PROJECT_REF>.supabase.co/storage/v1/object/<bucket>/<USER_ID>/existing-object.jpg" \
-H "apikey: <ANON_KEY>" \
-H "Authorization: Bearer <USER_ACCESS_TOKEN>"
# => 400, {"statusCode":"403","error":"Unauthorized","message":"Access denied"}
We confirmed via direct SQL that storage.objects.owner and owner_id were correctly set to the requesting user's ID for the object in question, so this isn't an ownership mismatch either.
Additional data points
- We tried both
storage_client2.5.1 and 2.6.0 (the latest at time of writing) — both sendmultipart/form-dataforupload()/uploadBinary()with an identical (empty) field name, and both hit the same failure. Upgrading the package version did not change the behavior. - The last successful upload we could find in our
postsbucket (viastorage.objects.created_at) was 2026-07-03, coincidentally the same daystorage_client2.6.0 was published on pub.dev — though we found no code difference between 2.5.1 and 2.6.0 that would explain it, so we suspect this is a backend/infrastructure-side change on Supabase's end rather than an SDK issue. - The requests that fail are consistently fast (not a timeout) — the server actively evaluates and rejects them, it isn't hanging or retrying.
Impact
Every image/file upload path in our app (Flutter, using supabase_flutter/storage_client) was broken — confirmed across 5 separate storage buckets covering multiple upload flows. Any app relying on the SDK's standard upload()/uploadBinary() methods, or on upsert: true, would hit this on this project.
Workaround in place
We now bypass the SDK's storage client entirely for uploads: raw binary POST with http.post(), to a freshly-generated (never-before-used, timestamped) path, with no x-upsert header. This works reliably. We're not attempting to overwrite/replace existing objects anymore (switched fixed filenames like avatar.jpg to timestamped ones) specifically to avoid the x-upsert/delete issues.
Ask
- Can you confirm whether something changed server-side around July 3, 2026 for storage multipart parsing and/or
x-upserthandling? - Could the
new row violates row-level security policy/ genericAccess deniedmessages be made more specific in these cases, so this is diagnosable without this level of manual isolation? - Is there a supported way to use
upsert: truereliably, or is raw binary + delete/replace-with-new-path the recommended pattern going forward?
Project ref: <PROJECT_REF> (will provide directly to Supabase support/staff, not posted publicly)
- Dominant language
- TypeScript
- Stars
- 1.3k
- Forks
- 314
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 89
Getting set up
- Ships a Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from supabase/storage
-
Preserve an explicit zero tenant file size limit on updatePossibly taken @hsusul claimed this today. Open
Difficulty 1/5 Under an hour Newbie friendliness 88/100
Maintainers usually reply within 1 day
-
Validate tenant integer limits as integersPossibly taken @hsusul claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Respect zero for tenant vector resource limitsPossibly taken @hsusul claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 73/100
Maintainers usually reply within 1 day
-
S3 HEAD/GET overwrite case-insensitive user metadata collisionsPossibly taken @ferhatelmas claimed this 15 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
supabase/storage#1461 · 1 assignee ·
Maintainers usually reply within 1 day
-
The sample application in storage article points to wrong URLMay be free again @prasadf18 claimed this 195 days ago, and no pull request is open. Opendocumentation
Difficulty 1/5 Under an hour Newbie friendliness 62/100
supabase/storage#557 · 1 comment ·
Maintainers usually reply within 1 day
All issues in supabase/storage
Similar issues
-
native
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
prime-radiant-inc/evener#4839 ·
Maintainers usually reply within 1 day
-
bug needs-triage
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
processone/fluux-messenger#1790 ·
Maintainers usually reply within 1 day
-
🛣️ Infrastructure
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
opencrvs/opencrvs-core#14232 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
kunchenguid/gh-axi#220 ·
Maintainers usually reply within 1 day