Skip to content

Security basics

"Row-level security policy for table objects" on upload

"New row violates row-level security policy for table objects" means your upload has no insert rule. Making the bucket public does not add one.

Vlad Tkachenko10 min read
A barred delivery slot with a document held outside it, and behind it a shelf of files with a lid drawn across the top.

In short

  • "New row violates row-level security policy for table objects" means your upload reached Supabase Storage and no rule on storage.objects allowed it in. The file was not stored, and the rest of the bucket is untouched.
  • Storage keeps its rules on one table for every bucket in your project, which is why the message names a table you never made.
  • Making the bucket public does not clear it. Supabase checks uploads either way, and public only decides who can open a file they already have the address of.
  • The rule an upload needs is an insert rule on storage.objects. If your code saves with upsert, it needs select and update as well.

Your app uploads a file. It worked in your builder's preview, or it worked last week, and now every attempt comes back with a sentence about row-level security:

new row violates row-level security policy for table "objects"

Most of that sentence will look familiar if you have already been through this on one of your own tables. The last word is the part that is different, because objects is not a table you made.

Here is the part that guide after guide gets wrong: the first fix you will find is to make the bucket public, and it does nothing to uploads. Supabase's own documentation says as much in one line. Flipping that toggle leaves the upload refused and opens the files that were already in there.

What "new row violates row-level security policy for table objects" means

Your upload reached Supabase Storage, Storage asked the rules on a table called storage.objects whether that file was allowed in, found none that said yes, and refused.

The row in the message is real. Storage keeps one row in storage.objects for every file it holds, carrying the file's name, which bucket it is in and who put it there. Writing a file means writing that row, so the rules on that table decide whether the upload happens at all. Nothing was lost: no file was stored, and the rest of the bucket sits exactly as it did a minute ago.

The wording comes from Postgres, the database engine underneath Supabase, which is why it reads like machinery. It carries the code 42501, the same code you get when a save into one of your own tables is refused.

Why the message names "objects" and not your bucket

Because Storage keeps the rules for every bucket in your project on that one table.

A bucket organises files. It does not hold rules of its own, and there is no policy editor attached to it. storage.objects is where the rules live, for all of your buckets at once, and bucket_id is a column on it. So a rule that says "only in the avatars bucket" is written as a condition on that column.

This is also why the table rule you wrote last week did not help. A rule on profiles is a rule about rows in profiles, and a file is a row in storage.objects.

Every bucket in your project is governed from the same table. The rules you wrote on your own tables sit on the other side of that divider and never meet a file.

Do I need to make the bucket public to upload?

No. Two different questions sit behind one bucket, and the toggle only answers one of them. Who may take a file out is what public decides. Who may put a file in is what your error is about, and Supabase's documentation is explicit that the setting does not reach it. From the page describing the two kinds of bucket, read on 11 October 2026:

Access control is still enforced for other types of operations including uploading, deleting, moving, and copying.

What public does do is just as plainly stated on that page: anyone who has the address of a file can open it without signing in. That is the whole of the setting.

So flipping it leaves you in the one state nobody wants. The upload is still refused, because uploading was never what the setting controlled, and the files that were already in the bucket can now be opened by anybody who has their addresses. What a public bucket actually costs you is a separate question from this error, and worth reading before you touch the switch.

What a bucket actually checks when you upload

One rule, and its name is insert.

Supabase's access-control page puts the default plainly: Storage does not allow any uploads to buckets without policies, and you allow operations selectively by writing them on storage.objects. Then it names the one you need:

For example, the only RLS policy required for uploading objects is to grant the INSERT permission to the storage.objects table.

There is a second half to that, and it is the commonest reason an insert rule does not clear the error:

To allow overwriting files using the upsert functionality you will need to additionally grant SELECT and UPDATE permissions.

If your upload passes upsert: true, which asks Storage to replace a file of the same name when one is already there, then an insert rule on its own is not enough. Replacing a file is three questions rather than one: may you add this row, may you see the row that is there, and may you change it.

What your app asks Storage to doWhat it needs on storage.objects
upload a new filean insert rule
replace a file of the same name (upsert)insert, and select and update as well
open a file in a private bucketa select rule
list what is in a bucketa select rule
delete a filea delete rule

You do not have to write all five. Write the ones your app actually does, which for most uploads is the first row and sometimes the second.

One store, two openings. The toggle is wired to the lower one. An upload arrives at the upper one, where a rule nobody has written yet decides whether it goes in.

The rule that lets your app upload, and only your app

Name the bucket, and say who the rule is for.

The documentation's own starting point restricts an upload to one bucket and to signed-in visitors:

create policy "Allow authenticated uploads"
on storage.objects
for insert
to authenticated
with check (bucket_id = 'my_bucket_id');

to authenticated is the part worth not skipping. It means the rule applies to visitors who are signed in; leave it out and the rule applies to everybody, strangers included.

For anything belonging to one particular person, the version Supabase publishes puts each person's files in a folder named after them:

create policy "Allow authenticated uploads"
on storage.objects
for insert
to authenticated
with check (
  bucket_id = 'my_bucket_id' and
  (storage.foldername(name))[1] = (select auth.jwt()->>'sub')
);

storage.foldername(name) splits a stored file's path into its folders, so [1] is the first one. auth.jwt()->>'sub' is the id of whoever is signed in and making the request. Read together, the two lines say that you may put a file in the folder named after you, in that bucket, and nowhere else.

Both of those are Supabase's examples rather than ours, read on 11 October 2026, with my_bucket_id left where they left it so you can see which part is the name of your own bucket.

If you would rather see the result from outside, our free scan asks your live project which of your buckets will hand their contents to a stranger. It takes about 20 seconds and needs no account: scan your app.

What a read rule hands over without being asked

Listing a bucket and downloading from it are the same privilege, so a rule written to make your downloads work also makes the bucket's contents listable.

Supabase's helper-function page says it directly: a single SQL privilege such as SELECT is used by multiple Storage actions. The access-control page then warns about the specific case, under an example that opens a bucket of avatars to everybody:

The allow_any_operation() filter is critical here as without it users would be able to list the bucket contents.

That is the gap we measure from outside. We have scanned 8,435 live apps that name a Supabase project. The bucket check got an answer out of 4,703 of them, and 792 of those answered a list request from us with the names of what was inside, with nobody signed in. That is about one in six of the apps we could ask.

A name is often all somebody needs, because a bucket that lists saves a stranger from guessing filenames. invoice-2026-03-hannah.pdf says who the file belongs to before anybody opens it.

The fix Supabase documents is to say which Storage action the rule is for:

create policy "Allow users to list their own objects"
on storage.objects
for select
to authenticated
using (
  storage.allow_only_operation('object.list')
  and owner_id = (select auth.uid()::text)
);

storage.allow_only_operation and its sibling storage.allow_any_operation are how a select rule narrows itself to one of the actions that share the privilege. Without one of them, a rule you wrote so that your app could show a picture is also a rule that will read out everything in the bucket.

One rule, two different Storage actions. The plate on the lower route is the operation filter, and the dashes are what it looks like when nobody wrote one.

Whether a listable bucket is a problem for your app depends on what is in it, which is a question the scan result cannot answer for you.

When public is the right answer

When the files are meant to be seen by anybody who asks, which is a real category, and Supabase lists its own examples of it.

Supabase's own example use cases for a public bucket are profile pictures, public media and blog post content. Those are files your app shows to a visitor who has not signed in, and serving them from a public bucket is faster as well, because the two kinds of bucket are cached differently.

For the other kind, the documentation names two ways to get a file out of a private bucket: a download carrying the signed-in person's token, or a signed link that works for a limited time. Both of those keep the decision with your rules rather than with whoever found the address.

Keeping the bucket right after today

Two things move after the rules are correct: somebody flips the toggle while chasing an unrelated bug, and a file goes missing.

Reeve Monitor runs the nine outside checks again for you:

  • all nine checks every hour, on up to three apps, including the bucket listing one
  • whether the app is up, every 60 seconds
  • a message when a result changes, so a bucket that opened last night does not wait for you to look
  • a monthly report of what it saw

Reeve Care keeps a copy of what is in there:

  • an encrypted copy of your Supabase database every night, kept where your project cannot reach it
  • each copy verified before it counts, by counting the rows in every table
  • your uploaded files as well, once you connect a Storage credential
  • a one-click restore when you need one
  • everything Monitor does

Both are on the pricing page, which is sometimes below the list figure and never above it.

What to do today

What to do

  • Read the message as a refusal. The file was not stored, the bucket is unchanged, and nothing needs recovering.
  • Add an insert rule on storage.objects that names your bucket in bucket_id and carries the TO clause you meant.
  • If your upload passes upsert: true, add select and update as well, or the insert rule alone will not clear the error.
  • Leave the public toggle where it is while you do this. It has no say over uploads and it does have a say over who can open what is already stored.
  • Read every select rule on storage.objects and ask what it allows besides the download you wrote it for. Listing shares that privilege.

Start with the bucket the error came from, then look at the other buckets in the same project, because a rule written broadly once tends to have been pasted twice. The 10-minute security checklist covers what else is usually left open in a newly launched app, and the Supabase safety guide goes through the rest of what a stranger can reach.

FAQ

Why does my Supabase upload fail with a row-level security error?

Because Supabase Storage asked the rules on a table called storage.objects whether your file was allowed in, and found no rule that said yes. Storage writes one row into that table for every file it keeps, and row-level security applies to that row exactly as it applies to a row in any other table. No file was stored and nothing already in the bucket changed. The message carries the Postgres code 42501, the same code you get when a save into your own table is refused.

Do I need to make my bucket public to upload?

No, and making it public will not help. Supabase documents that access control is still enforced for uploading, deleting, moving and copying whichever setting the bucket is on. Public changes one thing: whether somebody holding a file address can open that file without signing in. So the toggle leaves your upload refused and makes the files already in there openable by anyone who has their address.

What policies does a bucket need?

An upload needs one insert rule on storage.objects. If your code replaces files of the same name, which is what upsert does, Supabase says you also need select and update. Opening a file in a private bucket needs a select rule, and so does listing what is in the bucket, because both of those Storage actions run on the same SQL privilege. Deleting needs a delete rule. There is no requirement to write all four, only the ones your app actually does.

Is a public bucket dangerous?

It depends entirely on what is in it. Public is the correct setting for profile pictures, logos and anything else your app shows to a visitor who has not signed in, which is what Supabase lists as its own example use cases. It is the wrong setting for invoices, exports or anything belonging to one particular person, and for those a private bucket with a signed link is the shape you want. The question was never whether public is bad.

How do I check whether my bucket is listable?

Ask it from outside, with no account signed in, exactly as a stranger would. Listing is granted by a select rule rather than by the public toggle, so neither the dashboard toggle nor a glance at your policy list answers it. Our free scan does this on your live project and tells you which of your buckets answered with the names of what was inside.

Written by

Vlad Tkachenko

Founder, Reeve

I spend my time looking at apps built with Lovable, Bolt, v0, Cursor and Replit, and at the short list of mistakes that keep turning up in them.

More about the author

Read next

All articles

Not sure where your own app stands?

Run a free scan and get a plain-language grade from A to F in about 20 seconds. No account, no card.

Scan your app free

Automated external check, not a full audit. Absence of findings is not a guarantee of safety.