Supabase Storage upload by a restricted user: 403 "new row violates row-level security policy"
new row violates row-level security policy1What happened
A scheduled job uploads 3 files to a Supabase Storage bucket every 15 minutes, overwriting the old ones. It used to upload with the service key. We moved it to a restricted user, so it could only write those 3 files. The user signed in fine (200), but every upload got 403 "new row violates row-level security policy". The page the files feed went stale.
It was down almost 4 days in all. First the job lost its old key, because we switched that key off without checking that this job still read it (Oct 18 to Oct 21). Then the 403 (Oct 21 to Oct 22).
What didn't help:
Looking for a leftover code fix that never shipped: it had already shipped.
Checking the sign-in: it returned 200, so the login was fine.
The cause: the storage policies were written but never applied to the database.
2What we did
Applied 3 policies on storage.objects. The job overwrites files (an upsert, the x-upsert header), so it needs SELECT and UPDATE as well as INSERT.
create policy "uploader insert" on storage.objects for insert to authenticated with check (bucket_id = '<bucket>' and name in ('<file1>','<file2>','<file3>') and auth.uid() in ('<uploader-user-id>')); create policy "uploader select" on storage.objects for select to authenticated using (bucket_id = '<bucket>' and name in ('<file1>','<file2>','<file3>') and auth.uid() in ('<uploader-user-id>')); create policy "uploader update" on storage.objects for update to authenticated using (bucket_id = '<bucket>' and name in ('<file1>','<file2>','<file3>') and auth.uid() in ('<uploader-user-id>')) with check (bucket_id = '<bucket>' and name in ('<file1>','<file2>','<file3>') and auth.uid() in ('<uploader-user-id>'));Choices that matter:
Pin the user by its id, not its email.
Allow exact file names only. If the bucket is public, a wider rule lets the user write anything anyone can read.
No DELETE policy.
No owner check. The existing files were written by the service key and have no owner, so an owner check refuses the first overwrite.
3How we know it worked
On Thu Oct 22, after the policies went in, a test upload returned 200. Every run in the job's log from 7:15 to 8:30 AM PT (6 runs, every 15 minutes) logged all 3 files uploaded. The same user writing a file name outside the list still got 403, so it can't write anything else.
4What you can take from it
If a Storage upload gets this 403 and the sign-in works, read the policies on storage.objects. An overwrite needs INSERT, SELECT and UPDATE.
