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
- ✅ Session aktiv (
getUser())? - ✅ INSERT-Policy für den Bucket vorhanden?
- ✅ SELECT-Policy spiegelt dieselben Bedingungen?
- ✅ Upload-Pfad matcht die Policy (z. B.
{uid}/…)? - ✅ 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
- permission denied for table (42501)
- PGRST116: multiple (or no) rows
- Edge Functions CORS Fehler beheben
- Supabase: Users-Tabelle abfragen
- Supabase Storage File Upload: Sicher implementieren
- Supabase: infinite recursion detected in policy
- Supabase Auth: Invalid login credentials
- Supabase Edge Functions: Invalid JWT / 401
- RLS Insert-Fehler (Tabellen)
- Supabase Storage Access Control
Upload scheitert weiter mit 403? Kommentar mit Bucket-Policy (ohne Secrets) – dann finden wir die Lücke.
Kommentare