Supabase Storage upload 403: find the RLS boundary

An upload permission error is not a reason to make every file public. Identify the failing request, distinguish a new file from an overwrite, and check the identity and policy that actually apply.

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.

Editorial triage from official error and access-control docs; not a diagnosis of your account
Observed resultBoundary to inspectNext check
403 with a row-level security messageRequest identity and policies on storage.objectsCompare the exact bucket, object path and operation with the policy conditions.
New filename works; replacing a file failsOverwrite behavior / upsertCheck SELECT and UPDATE as well as INSERT; keep the same user and path during review.
InvalidJWT / 401AuthenticationCheck the session used by the upload client before editing policies.
Asset already exists / duplicate / 400 or 409Existing object at that pathChoose a new path if replacement is not intended; this is not the same RLS diagnosis.
413 / EntityTooLarge, or InvalidMimeTypeFile restrictionsCheck size and permitted MIME types; broader database permissions do not solve this branch.
Public image opens, but uploading failsRead access versus write accessPublic 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

New-object upload with upsert false needs an applicable INSERT policy. An intentional overwrite with upsert true additionally needs SELECT and UPDATE; inspect identity, bucket and path in either branch.
Original editorial operation map, not Supabase UI, a network trace or a completed permission test. These are the documented standard-upload requirements, not universal policy SQL.

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

  1. 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.
  2. 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.
  3. 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.
  4. Operation. In the correct project's Storage policy view, inspect policies on storage.objects for 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.

Record the failing upload, identify the operation and session, approve a scoped change, verify an allowed test user and a forbidden test user, then review the result before production approval.
Original editorial verification checklist. It depicts proposed checks, not a repaired account or evidence that isolation has already been tested.
  • 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.