# Supabase Storage upload by a restricted user: 403 "new row violates row-level security policy"

Problem 25. Solved Oct 22. Last checked Oct 22. Tags: supabase, storage, rls, permissions.
Page: https://bluhook.com/log/supabase-storage-upload-403-rls
Exact text to search: new row violates row-level security policy

## What 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:
1. Looking for a leftover code fix that never shipped: it had already shipped.
2. 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.

## What we did

1. 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.

   ```sql
   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:
   1. Pin the user by its id, not its email.
   2. Allow exact file names only. If the bucket is public, a wider rule lets the user write anything anyone can read.
   3. No DELETE policy.
   4. No owner check. The existing files were written by the service key and have no owner, so an owner check refuses the first overwrite.

## How 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.

## What 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.
