Design wechseln

Supabase Storage in der Praxis: Upload, Berechtigungen und CDN-Beschleunigung

Easton editorial illustration: orchestration hub with branches

Ich starre auf die Fehlermeldung in der Konsole. Die Avatar-Upload-Funktion ist seit einer halben Stunde live – und schon melden Nutzer: Alle Avatare zeigen plötzlich dasselbe Gesicht.

Die Ursache: fehlende RLS-Policies für Storage. Der Bucket war öffentlich, Upload-Pfade ohne Nutzerisolation – jeder Upload konnte fremde Dateien überschreiben. Ein Berechtigungsfehler, der beinahe ein Produktionsvorfall geworden wäre.

Supabase Storage wirkt einfach – aber Berechtigungen, CDN-Beschleunigung und Bildtransformation haben viele Fallstricke. Dieser Artikel fasst die Erfahrungen und Lessons Learned zusammen.

1. Schnellstart: Standard-Datei-Upload

Zuerst das Grundlegende: Dateien hochladen.

Bucket erstellen

Öffnen Sie die Supabase-Konsole, wählen Sie links Storage und klicken Sie auf New bucket. Vergeben Sie einen Namen – z. B. avatars für Profilbilder, posts für Artikelbilder. Die Option Make this bucket public? lassen Sie vorerst deaktiviert; im Berechtigungskapitel erklären wir den Unterschied.

Meine Regel: Private Buckets für sensible Dateien, Public Buckets für statische Assets. Standardmäßig erstelle ich private Buckets und passe bei Bedarf an.

SDK-Upload-Code

Vorausgesetzt, @supabase/supabase-js ist installiert – der Code ist überschaubar:

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  'https://your-project.supabase.co',
  'your-anon-key'
)

// Datei hochladen
async function uploadFile(file: File) {
  const filePath = `uploads/${Date.now()}-${file.name}`

  const { data, error } = await supabase.storage
    .from('avatars')  // Bucket-Name
    .upload(filePath, file, {
      cacheControl: '3600',  // 1 Stunde Cache
      upsert: false  // Fehler, wenn Datei existiert – kein Überschreiben
    })

  if (error) {
    console.error('Upload fehlgeschlagen:', error.message)
    return null
  }

  return data.path  // Dateipfad zurückgeben
}

Ehrlich gesagt habe ich diesen Code mindestens zehnmal geschrieben. Entscheidend ist das Design von filePath – später erklären wir den Zeitstempel-Prefix und die Nutzerisolation.

Dateigrößen-Limits

Laut Dokumentation unterstützt Standard-Upload bis zu 5 GB. In der Praxis: Unter 6 MB ist Standard-Upload am besten; darüber empfiehlt sich TUS mit Resume.

Was ist TUS? Kurz gesagt: Protokoll für große Uploads mit Fortsetzung nach Unterbrechung. Netz weg – nach Reconnect geht es weiter, ohne von vorn zu starten. Für Videos und große Bilder ein deutlicher UX-Gewinn – stellen Sie sich 90 % Fortschritt und dann Verbindungsabbruch vor.

TUS erfordert zusätzliche Konfiguration. Wenn Sie es noch nicht brauchen, reicht Standard-Upload für die meisten Fälle.

// TUS-Upload-Beispiel (empfohlen für große Dateien)
const { data, error } = await supabase.storage
  .from('videos')
  .upload('large-video.mp4', file, {
    duplex: 'half',  // Streaming-Upload aktivieren
    // TUS übernimmt Resume automatisch
  })

2. Sichere Konfiguration: RLS-Policies im Detail

Zurück zu meinem Fehler um drei Uhr nachts – keine Berechtigungen, Dateien konnten beliebig überschrieben werden.

Supabase Storage nutzt wie die Datenbank PostgreSQL. Berechtigungen laufen über RLS (Row Level Security). Ein Bucket entspricht einer Tabelle, jede Datei einer Zeile.

Public vs. Private Bucket

Bei der Erstellung wählen Sie Public bucket oder Private bucket.

Public bucket: Lesbar für alle ohne Authentifizierung – geeignet für öffentliche Avatare, Logos, statische Assets.

Private bucket: Zugriff nur mit Authentifizierung. Wichtig: Authentifizierung allein reicht nicht – wer lesen, schreiben oder löschen darf, legen RLS-Policies fest.

Mein Rat: Standardmäßig Private Bucket. Erst Berechtigungen sauber setzen, dann bei Bedarf öffnen – sicherer als nachträglich absichern.

Arten von RLS-Policies

Auf der Policy-Seite von Storage gibt es vier Operationen:

  • SELECT: Datei lesen (Download, URL abrufen)
  • INSERT: Neue Datei hochladen
  • UPDATE: Bestehende Datei aktualisieren/überschreiben
  • DELETE: Datei löschen

Jede Operation lässt sich separat per Policy steuern. Typisches Muster:

-- Nutzer dürfen nur eigene Dateien verwalten
CREATE POLICY "Users manage own files"
ON storage.objects FOR ALL
USING (auth.uid()::text = (storage.foldername(name))[1]);

Das SQL wirkt komplex – aufgeschlüsselt:

  • auth.uid() liefert die ID des angemeldeten Nutzers
  • storage.foldername(name) extrahiert das erste Verzeichnis im Pfad
  • Bei user123/avatar.jpg ist das erste Segment user123

Die Policy erlaubt Operationen nur, wenn das erste Pfadsegment der Nutzer-ID entspricht – das ist die Kernidee der Nutzerisolation.

Nutzerisolation umsetzen

Beim Upload die Nutzer-ID als erstes Pfadsegment:

async function uploadAvatar(userId: string, file: File) {
  // Pfad: Nutzer-ID/Dateiname
  const filePath = `${userId}/avatar-${Date.now()}.jpg`

  const { data, error } = await supabase.storage
    .from('avatars')
    .upload(filePath, file)

  return data?.path
}

Jeder Nutzer landet in seinem „Ordner“. RLS erlaubt nur Pfade mit eigener ID – fremde Dateien sind tabu.

Signierte Zugriffs-URLs erzeugen

Dateien in Private Buckets liefern ohne Signatur 404. Lösung:

// Temporärer Link (1 Stunde gültig)
const { data, error } = await supabase.storage
  .from('avatars')
  .createSignedUrl('user123/avatar.jpg', 3600)

console.log(data?.signedUrl)  // Vollständige signierte URL

Die Gültigkeitsdauer wählen Sie selbst. Zu lang ist riskant, zu kurz nervt. 1–4 Stunden sind ein guter Mittelweg.

Für öffentliche Dateien ohne Bucket-Umstellung: getPublicUrl:

const { data } = supabase.storage
  .from('public-assets')
  .getPublicUrl('logo.png')

// Diese URL braucht keine Signatur – für alle erreichbar

Häufige Policy-Fallstricke

Ein paar Stolpersteine aus der Praxis:

  1. INSERT-Policy vergessen: Nutzer ist eingeloggt, Upload schlägt fehl – Fehler: „new row violates row-level security policy“

  2. Policy zu weit gefasst: USING (true) erlaubt allen alles – effektiv wie ohne RLS.

  3. Schlechtes Pfaddesign: Liegt die Nutzer-ID nicht im ersten Segment, liefert foldername das falsche Ergebnis. Ich hatte uploads/user123/file.jpg – extrahiert wurde uploads, die Policy griff ins Leere.

Policies zuerst im SQL-Editor der Konsole testen, dann in Produktion übernehmen.

3. Performance: Smart CDN und Bildtransformation

Upload und Berechtigungen stehen – als Nächstes: schnellere Auslieferung.

Smart CDN – Funktionsweise

Supabase Smart CDN ist kein klassisches CDN. Es passt die Cache-Strategie an die Zugriffshäufigkeit an: beliebte Dateien länger, selten genutzte kürzer.

Laut Dokumentation synchronisiert sich Cache-Invalidierung weltweit in maximal 60 Sekunden. Ein Update in Tokio ist in New York innerhalb einer Minute sichtbar – deutlich schneller als viele traditionelle CDNs mit Minuten oder Stunden.

Smart CDN ist kostenpflichtig – Pro Plan ($25/Monat). Im Free Plan funktionieren Upload und Download, aber ohne CDN – direkt von Supabase-Servern.

Bildtransformations-Parameter

Praktisches Feature: Skalierung und Zuschnitt per URL-Parameter, ohne eigene Bildpipeline.

Basisparameter:

?width=300&height=200  // Zielgröße
?resize=contain        // Seitenverhältnis beibehalten, kein Zuschnitt
?resize=cover          // Bereich füllen, überschüssige Teile abschneiden
?quality=80            // Qualität (1–100)
?format=webp           // WebP – kleinere Dateigröße

Kombiniert:

const baseUrl = supabase.storage
  .from('avatars')
  .getPublicUrl('user123/avatar.jpg').data.publicUrl

// Thumbnail erzeugen
const thumbnailUrl = `${baseUrl}?width=100&height=100&resize=cover`

Limits der Bildtransformation:

  • Größe: 1 bis 2500 Pixel
  • Originaldatei: maximal 25 MB
  • Formate: JPEG, PNG, WebP, GIF, AVIF

Bei Überschreitung gibt es Fehler. Ein 30-MB-Original zum Skalieren – bei mir abgelehnt.

Abrechnung: Freikontingent pro Projekt

Bildtransformation wird pro Transformation abgerechnet, nicht nach Speichervolumen.

Pro Projekt und Monat sind die ersten 100 Transformationen kostenlos. Danach $5 pro 1.000 Transformationen.

Für persönliche Projekte oder kleine Teams reichen 100 meist. Mein Blog braucht monatlich nur ein paar Dutzend Avatare und Beitragsbilder. Nur bei bildlastigen Apps à la Instagram lohnt genaueres Kalkulieren.

Next.js-Integration: Image Loader

Mit Next.js können Sie einen Supabase Image Loader konfigurieren – next/image hängt Transformationsparameter automatisch an:

// next.config.js
module.exports = {
  images: {
    loader: 'custom',
    loaderFile: './supabase-image-loader.js',
  }
}

Loader-Datei:

// supabase-image-loader.js
export default function supabaseLoader({ src, width, quality }) {
  const params = new URLSearchParams()
  params.set('width', width.toString())
  params.set('quality', (quality || 75).toString())
  params.set('format', 'webp')

  return `${src}?${params.toString()}`
}

Mit <Image src="..." width={300} /> in Next.js werden die Parameter automatisch gesetzt.

Pro-Plan-Schwelle

Smart CDN und Bildtransformation erfordern den Pro Plan. Free Plan: Basis-Upload und -Download.

Upgrade sinnvoll? Hängt vom Projekt ab. Für wenige Avatare reicht Free Plan. Bei vielen Bildern und Performance-Fokus spart Pro Plan CDN und Transformation – ohne eigene CDN- oder Bildverarbeitungs-Infrastruktur.

Mein Vorgehen: Vor dem Launch Free Plan zum Testen, nach stabilem Traffic Upgrade auf Pro. $25/Monat sind kein Kleingeld.

4. Praxisbeispiel: Vollständige Blog-Konfiguration

Theorie ist gut – hier die Storage-Konfiguration meines Blogs, von null bis produktionsreif.

Szenario: Nutzer-Avatare + Artikelbilder

Zwei Buckets:

  • avatars: Profilbilder, Private Bucket – Nutzer verwalten nur eigene Avatare
  • post-images: Artikelbilder, Private Bucket – Autoren laden hoch, Lesen für alle (per signierte URL)

Schritt 1: Buckets erstellen

In der Konsole:

  1. Storage > New bucket > Name avatars, Private aktivieren
  2. Gleiches für post-images

Schritt 2: RLS-Policies konfigurieren

Policies für avatars:

-- Alle dürfen Avatare lesen (öffentliches Lesen)
CREATE POLICY "Anyone can view avatars"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars');

-- Nutzer dürfen nur eigene Avatare hochladen und aktualisieren
CREATE POLICY "Users manage own avatar"
ON storage.objects FOR INSERT
WITH CHECK (bucket_id = 'avatars' AND auth.uid()::text = (storage.foldername(name))[1]);

-- Nutzer dürfen nur eigene Avatare löschen
CREATE POLICY "Users delete own avatar"
ON storage.objects FOR DELETE
USING (bucket_id = 'avatars' AND auth.uid()::text = (storage.foldername(name))[1]);

Policies für post-images:

-- Autoren dürfen Artikelbilder hochladen (Rolle author im JWT)
CREATE POLICY "Authors can upload post images"
ON storage.objects FOR INSERT
WITH CHECK (
  bucket_id = 'post-images'
  AND auth.jwt() ->> 'role' = 'author'
);

-- Alle dürfen Artikelbilder lesen
CREATE POLICY "Public read post images"
ON storage.objects FOR SELECT
USING (bucket_id = 'post-images');

Schritt 3: Frontend-Upload-Code

Avatar-Upload-Komponente:

async function handleAvatarUpload(file: File) {
  const user = await supabase.auth.getUser()
  if (!user.data.user) return alert('Bitte zuerst anmelden')

  // Pfad: Nutzer-ID/avatar.jpg (fester Name – jeder Upload überschreibt das alte Bild)
  const filePath = `${user.data.user.id}/avatar.jpg`

  const { error } = await supabase.storage
    .from('avatars')
    .upload(filePath, file, { upsert: true })

  if (!error) {
    // Public URL (SELECT-Policy erlaubt Lesen für alle)
    const url = supabase.storage.from('avatars').getPublicUrl(filePath)
    setUserAvatar(url.data.publicUrl)
  }
}

Artikelbild-Upload:

async function handlePostImageUpload(file: File) {
  const filePath = `posts/${Date.now()}-${file.name}`

  const { data, error } = await supabase.storage
    .from('post-images')
    .upload(filePath, file)

  if (!error) {
    // Signierte URL, 24 Stunden gültig
    const { data: urlData } = await supabase.storage
      .from('post-images')
      .createSignedUrl(filePath, 86400)

    insertImageToEditor(urlData?.signedUrl)
  }
}

Schritt 4: Tests vor dem Go-live

Vor dem Launch prüfen:

  1. Sehen nicht angemeldete Nutzer Artikelbilder? (Ja – SELECT-Policy erlaubt es)
  2. Können normale Nutzer Artikelbilder hochladen? (Nein – nur Rolle author)
  3. Kann Nutzer A den Avatar von Nutzer B überschreiben? (Nein – Pfadisolation)

Jeden Punkt einmal durchspielen. Die Lektion um drei Uhr nachts will ich nicht wiederholen.

Fazit

Bei Supabase Storage geht es um drei Dinge: Upload, Berechtigungen, Beschleunigung.

Upload ist am einfachsten – wenige Zeilen Code. Bei Berechtigungen lohnt Sorgfalt: RLS-Policies einmal konfigurieren reicht nicht – sie müssen zum Geschäftsmodell passen und getestet werden. CDN und Bildtransformation sind Extras im Pro Plan, sparen aber Entwicklungszeit.

Mein Ansatz: Zuerst Upload und Berechtigungen stabil und sicher. Bei Bedarf CDN, bei Bedarf Transformation. Schrittweise statt alles auf einmal.

Wenn Sie Supabase Storage nutzen – teilen Sie gern Ihre Fallstricke. Mit meiner Erfahrung um drei Uhr nachts bin ich vermutlich nicht allein.

Supabase Storage – vollständiger Konfigurationsablauf

Praxisleitfaden von Bucket-Erstellung über Berechtigungen bis zur CDN-Beschleunigung

⏱️ Estimated time: 30 min

  1. 1

    Step 1: Bucket erstellen

    Privaten Bucket in der Supabase-Konsole anlegen:

    • Storage > New bucket
    • Name eingeben (z. B. avatars)
    • Private aktivieren (empfohlen als Standard)
    • Create bucket klicken
  2. 2

    Step 2: RLS-Policy konfigurieren

    Row Level Security für den Bucket einrichten:

    • Storage > Bucket wählen > Policies
    • New Policy klicken
    • Operation wählen (SELECT/INSERT/UPDATE/DELETE)
    • Policy-Regel schreiben (z. B. Nutzerisolation)
    • Testen und in Produktion anwenden
  3. 3

    Step 3: Dateien hochladen

    Upload per SDK:

    • Pfadstruktur planen (z. B. userId/filename)
    • storage.from().upload() aufrufen
    • cacheControl und upsert setzen
    • Fehler und Rückgabepfad behandeln
  4. 4

    Step 4: CDN und Bildtransformation (optional)

    Mit Pro Plan verfügbar:

    • Smart CDN cached beliebte Dateien automatisch
    • Bildtransformation per URL-Parameter (width/height/format)
    • Next.js Image Loader integrieren
    • Freikontingent im Blick behalten (100 Bilder/Monat)

FAQ

Was ist der Unterschied zwischen Public und Private Bucket?
Public Buckets sind für jeden lesbar – geeignet für Logos, öffentliche Avatare usw. Private Buckets erfordern Authentifizierung; konkrete Rechte steuern RLS-Policies – deutlich sicherer.
Wie konfiguriere ich RLS-Policies für Nutzerisolation?
Kernidee: Nutzer-ID als erstes Pfadsegment:

• Upload-Pfad: userId/filename
• Policy: auth.uid()::text = (storage.foldername(name))[1]
• Nutzer dürfen nur Pfade mit eigener ID bearbeiten
Wie teile ich Dateien aus einem Private Bucket extern?
Mit createSignedUrl() eine signierte temporäre URL erzeugen – Gültigkeitsdauer frei wählbar. Zu lang ist unsicher, zu kurz schlecht für die UX; 1–4 Stunden sind ein guter Kompromiss.
Welche Upload-Limits gibt es?
Standard-Upload maximal 5 GB. Unter 6 MB ist Standard-Upload am angenehmsten; darüber empfiehlt sich TUS mit Resume – nach Netzabbruch geht der Upload dort weiter, wo er aufgehört hat.
Welche Parameter und Limits hat die Bildtransformation?
Unterstützte Parameter:

• width/height: Größe (1–2500 Pixel)
• resize: contain (Seitenverhältnis) oder cover (Zuschnitt)
• quality: Qualität (1–100)
• format: webp/jpeg/png/gif/avif

Limit: Originaldatei maximal 25 MB
Kosten Smart CDN und Bildtransformation extra?
Ja, Pro Plan ($25/Monat) erforderlich. Im Free Plan nur Basis-Upload/-Download. Bildtransformation: 100 Bilder/Monat pro Projekt kostenlos, danach $5 pro 1.000 Transformationen.

8 Min. Lesezeit · Veröffentlicht am: 9. Apr. 2026 · Aktualisiert am: 14. Juli 2026

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog