Supabase Storage: 403 "new row violates row-level security policy" beim Upload

7. August 2026 ca. 4 Min. Lesezeit Supabase
Inhalt 10

Upload-Code sieht richtig aus, der User ist eingeloggt – und trotzdem antwortet Supabase Storage mit:

StorageApiError: new row violates row-level security policy
statusCode: 403

Oft hast du bereits eine INSERT-Policy – und trotzdem scheitert der Upload. Hier ist der Fix, den die meisten übersehen.

Das Problem: INSERT reicht nicht aus

Beim Upload macht die Storage-API intern ungefähr: INSERT … RETURNING * auf storage.objects. Ohne passende SELECT-Policy darf Postgres die neue Zeile nicht zurückgeben – die ganze Transaktion scheitert mit RLS/403, obwohl das Insert „eigentlich“ erlaubt wäre.

Die Lösung: SELECT-Policy spiegeln

Schritt 1: Bucket und Auth prüfen

const { data, error } = await supabase.storage
  .from('avatars')
  .upload(`${user.id}/avatar.png`, file, {
    upsert: true,
    contentType: file.type,
  })

if (error) console.error(error.message, error.statusCode)

User muss eine Session haben (außer der Bucket ist bewusst public und Policies erlauben anon).

Schritt 2: INSERT- und SELECT-Policies anlegen

-- Uploads für den eigenen Ordner
CREATE POLICY "avatars_insert_own"
ON storage.objects FOR INSERT TO authenticated
WITH CHECK (
  bucket_id = 'avatars'
  AND (storage.foldername(name))[1] = auth.uid()::text
);

-- KRITISCH: ohne SELECT scheitert RETURNING *
CREATE POLICY "avatars_select_own"
ON storage.objects FOR SELECT TO authenticated
USING (
  bucket_id = 'avatars'
  AND (storage.foldername(name))[1] = auth.uid()::text
);

-- Optional für upsert/update
CREATE POLICY "avatars_update_own"
ON storage.objects FOR UPDATE TO authenticated
USING (
  bucket_id = 'avatars'
  AND (storage.foldername(name))[1] = auth.uid()::text
)
WITH CHECK (
  bucket_id = 'avatars'
  AND (storage.foldername(name))[1] = auth.uid()::text
);

Schritt 3: Pfad und Policy müssen zusammenpassen

Wenn die Policy den ersten Ordner als auth.uid() erwartet, muss der Upload-Pfad so aussehen:

const path = `${session.user.id}/${file.name}`
await supabase.storage.from('avatars').upload(path, file)

Pfad avatar.png ohne User-Ordner → Policy greift nicht → 403.

Häufige Fehler

  • Nur INSERT-Policy: Klassiker für genau diese Fehlermeldung.
  • Falscher bucket_id-String: Tippfehler zwischen Dashboard-Bucket und Policy.
  • Policy für public, Upload als authenticated: Rollen müssen zur Session passen.
  • upsert ohne UPDATE-Policy: Erster Upload ok, Überschreiben knallt.

Policies im Dashboard setzen

Storage → Bucket → Policies → New policy. Für den Schnellstart: „Allow authenticated uploads to own folder“ und danach explizit eine gleichwertige SELECT-Policy ergänzen.

Troubleshooting Checklist

  1. ✅ Session aktiv (getUser())?
  2. ✅ INSERT-Policy für den Bucket vorhanden?
  3. ✅ SELECT-Policy spiegelt dieselben Bedingungen?
  4. ✅ Upload-Pfad matcht die Policy (z. B. {uid}/…)?
  5. ✅ Bei upsert: UPDATE-Policy vorhanden?

Fazit

Storage-403 mit new row violates row-level security policy ist fast immer eine fehlende oder zu enge SELECT-Policy auf storage.objects – nicht (nur) ein Insert-Problem. Policy spiegeln, Pfad angleichen, fertig.

Weitere Ressourcen

Upload scheitert weiter mit 403? Kommentar mit Bucket-Policy (ohne Secrets) – dann finden wir die Lücke.

Kommentare