Storage error: 403 Forbidden: 'new row violates row-level security policy' on upload
If you are observing a 403 Forbidden error with the message new row violates row-level security policy when uploading files, it typically indicates that the database cannot return the metadata for the newly created object. This can happen even if your INSERT policies are correctly defined and the user’s JWT is valid.
Important: this only happens when upsert: true
This error occurs specifically when your upload call sets upsert: true. If you’re not intentionally overwriting existing files, leave upsert at its default value of false (or set it explicitly). With upsert: false, Supabase Storage only needs to perform an INSERT, no row metadata needs to be read back, and a correctly-scoped INSERT policy is all that’s required.
If you don’t need overwrite behavior, you don’t need a SELECT policy at all. Trying to upload a file that already exists with upsert: false will instead fail with a 400 “Asset already exists” error, not the 403 RLS error described here.
const avatarFile = event.target.files[0]
const { data, error } = await supabase
.storage
.from('avatars')
.upload('public/avatar1.png', avatarFile, {
cacheControl: '3600',
upsert: false // default behavior — no SELECT policy needed
})
Why Does This Happen (when upsert: true)?
When upsert is set to true, the Supabase Storage API needs to check whether the object already exists, and if it does, update it in place. To do this, it performs the upload as an INSERT ... ON CONFLICT operation followed by a RETURNING * clause, so it can return the object’s details back to the client.
If a SELECT RLS policy is missing or does not cover the object being uploaded, the database is unable to return the row metadata for that RETURNING * clause. This causes the entire transaction to fail with a 403 policy violation, even though the underlying INSERT or UPDATE would otherwise have been allowed.
How to Resolve:
First, check whether you actually need upsert: true:
- If you don’t need to overwrite existing files, set
upsert: false(or omit it, since this is the default). No SELECT policy is required. Attempting to upload a file at a path that already exists will return a 400 “Asset already exists” error instead, which you can handle in your app logic (e.g., prompt the user to choose a different name, or delete the old file first). - If you do need to overwrite existing files, keep
upsert: trueand add a SELECT policy onstorage.objectsthat mirrors your INSERT policy, so the authenticated user can read back the object they just wrote.
For policy examples covering common patterns (restricting by bucket, folder, or user ID), see the Storage Access Control - Policy Examples section of the docs.
You can manage your RLS policies via the Dashboard or the SQL editor.