Quick answer: first upload or overwrite?
For new row violates row-level security policy, first check the authenticated request, exact object path and upsert option. A new-object upload needs an applicable INSERT policy. Overwriting adds SELECT and UPDATE requirements. Do not add a broad read policy to every upload simply because the message mentions RLS.
The official 403 guide describes a particular upsert: true case: a missing matching SELECT policy prevents the object metadata from being returned. It does not say that every Storage 403 has this cause. A missing INSERT policy or the wrong request identity still needs a different review.
Match the error before changing permissions
Use the failed Storage request in browser developer tools or your authorized server logs. Record its time, project, method, HTTP status and returned code/message. Keep token values, signed URLs, filenames containing personal data and file contents out of shared reports. The SDK can return an error alongside data; a rendered success toast alone is insufficient.
| Observed result | Boundary to inspect | Next check |
|---|---|---|
| 403 with a row-level security message | Request identity and policies on storage.objects | Compare the exact bucket, object path and operation with the policy conditions. |
| New filename works; replacing a file fails | Overwrite behavior / upsert | Check SELECT and UPDATE as well as INSERT; keep the same user and path during review. |
| InvalidJWT / 401 | Authentication | Check the session used by the upload client before editing policies. |
| Asset already exists / duplicate / 400 or 409 | Existing object at that path | Choose a new path if replacement is not intended; this is not the same RLS diagnosis. |
| 413 / EntityTooLarge, or InvalidMimeType | File restrictions | Check size and permitted MIME types; broader database permissions do not solve this branch. |
| Public image opens, but uploading fails | Read access versus write access | Public delivery does not grant permission to upload or overwrite. |
Supabase's error reference distinguishes authentication, permission, duplicate-object and file-limit failures. Legacy and newer responses differ: the official troubleshooting example describes a duplicate as 400, while the error reference also lists 409 conflict codes. Preserve the actual code and body instead of diagnosing from the status number alone.
Check upsert before adding a SELECT policy
If the feature should only add a new file, keep upsert: false and use an approved unused object path. Handle a duplicate explicitly. Do not delete an existing file or switch to overwrite just to silence an error; those choices change product behavior.
// Shape to review in your existing app, not a complete upload form.
// Requires an existing user-session client, approved bucket and file.
const result = await supabase.storage
.from(bucketId)
.upload(newObjectPath, file, { upsert: false });
if (result.error) {
// Handle the error in the app; do not log tokens or private file data.
showUploadFailure();
}This illustrates the JavaScript upload API, not a tested patch for your app. bucketId names the existing bucket; newObjectPath is the object path inside it. The caller must already have the intended session and permission. No credentials belong in this example.
If replacement is intentional, inspect the policies for INSERT, SELECT and UPDATE on storage.objects. Compare their scope against the same authenticated user, bucket and path. The access-control guide is the policy reference; the upsert-specific metadata explanation is only one reason that an otherwise plausible INSERT policy is insufficient.
Compare the actual request with the policy
- Project and client. Confirm the upload targets the intended project and uses the client that holds the current user session. A successful login in another tab, client instance or environment does not verify this request.
- Identity. Have the maintainer inspect whether a valid user session is available at upload time. Record the expected user and role privately, not the JWT. If authentication is failing, solve that branch before widening authorization.
- Resource. Compare the bucket ID and full object path with the policy's bucket, folder or owner restrictions. An upload to a different prefix needs an explanation, not a policy that allows every prefix.
- Operation. In the correct project's Storage policy view, inspect policies on
storage.objectsfor the operation actually used. Ask whether the intended user can create that object and, when replacing it, read and update it. Preserve the previous definition before any separately approved edit.
The API-key guide separates application access from user identity: a publishable key does not prove that a signed-in user's session accompanied the upload. Secret keys and legacy service_role keys bypass RLS and belong in controlled server environments. An admin-path success is not an equivalent test of the ordinary user path.
If the project itself sends users to localhost or loses its session after returning from login, continue with the Supabase login redirect and session checklist. This upload guide does not replace that authentication diagnosis.
Why does a public bucket still return an upload 403?
Public delivery and write authorization are separate. Supabase's bucket documentation says a public asset URL permits retrieval, while uploading, deleting, moving and copying still enforce access control. Opening an image successfully therefore proves little about the uploader's permissions.
Keep a private bucket private when that matches the product. Do not disable RLS, expose a server secret or add unrestricted policies to make a test pass. Choose the smallest approved policy change that supports the intended actor and operation. The examples in this article deliberately do not create or alter policies.
Verify the fix without opening other users' files
The following is a proposed acceptance exercise for an authorized isolated preview with disposable files and test users. ShipStackLab has not run it. Agree the expected access first; never use customer records or a secret-key client to stand in for a normal user.
- Allowed path: confirm the intended user can upload a harmless new file to the approved path and that the app handles the returned result.
- Overwrite behavior: confirm a duplicate is rejected when replacement is forbidden. Where replacement is intended, use a disposable object and verify only the authorized owner can replace it.
- Denied path: a second test user must not gain access to another user's private object. Check signed-out behavior against the product's actual rules too.
- Delivery: inspect the intended file through the permitted application path. An upload success does not establish that private download, listing or sharing permissions are correct.
- Release: review the exact code/policy diff, recovery owner and test record before seeking production approval. Deployment and verification on the real host are later steps.
Project / environment / app revision / SDK version: Request time / status / returned code and sanitized message: Expected user role / bucket / redacted object-path pattern: New object or overwrite / upsert value: Relevant policy names / reviewed scope / approved change: Allowed test / duplicate test / forbidden-user test: File delivery result / recovery owner / remaining UNKNOWN: Production approval and later verification:
For recovery planning before broader data changes, use the backup and Storage-file inventory. For platform boundaries, return to the Supabase profile. Keep the diagnosis attached to the observed request; do not treat a generic 403 as proof of a particular policy defect.