Direkt zum Inhalt
NikoFischer.com

Main navigation

  • Startseite
  • Über mich
    • My Reading List
    • Recommended Youtube Channels
    • Life Rules
    • Podcast
  • 50-Tage Challenge
  • Impressum
Sprachumschalter
  • German
  • English

Pfadnavigation

  1. Startseite

Supabase Storage: File Upload richtig implementieren

🎸
🚀 Beta läuft

PYNGUP: Rebellion gegen toxische Produktivität

Beta auf 100 Plätze begrenzt. Tasks werden zu sozialen Commitments statt einsamer To-Dos.

🚀 Beta beitreten 📖 Story lesen "€487 verschwendet"

File Uploads gehören zu den häufigsten Features in modernen Web-Apps, aber die korrekte Implementierung mit Supabase Storage kann trickreich sein. Viele Entwickler kämpfen mit Permission-Fehlern, langsamen Uploads oder unsicheren Konfigurationen.

In diesem Artikel zeige ich dir Schritt für Schritt, wie du File Uploads mit Supabase Storage sicher und performant implementierst - von der Bucket-Konfiguration bis zur Production-reifen Frontend-Integration.

Was ist Supabase Storage?

Supabase Storage ist ein S3-kompatibles Object Storage System, das nahtlos in dein Supabase-Projekt integriert ist. Es bietet:

  • Row Level Security für sichere File-Zugriffe
  • Automatische Image-Optimierung und Transformation
  • CDN-Integration für schnelle Auslieferung
  • Resumable Uploads für große Dateien
  • Webhook-Integration für automatische Verarbeitung

Schritt 1: Storage Bucket einrichten

Bucket erstellen

Gehe zu deinem Supabase Dashboard → Storage und erstelle einen neuen Bucket:

-- Option 1: Über das Dashboard (empfohlen für Anfänger)
-- Storage > Create bucket > "uploads" > Public/Private auswählen

-- Option 2: Per SQL
INSERT INTO storage.buckets (id, name, public)
VALUES ('uploads', 'uploads', false);

Wichtig: Setze public nur auf true, wenn alle Dateien öffentlich zugänglich sein sollen (z.B. für Produktbilder).

RLS Policies konfigurieren

Das ist der kritische Teil! Ohne korrekte RLS Policies funktioniert der Upload nicht:

-- Policy für Upload (INSERT)
CREATE POLICY "Authenticated users can upload files"
ON storage.objects
FOR INSERT
TO authenticated
WITH CHECK (
  bucket_id = 'uploads' AND
  (storage.foldername(name))[1] = auth.uid()::text
);

-- Policy für eigene Dateien lesen (SELECT)
CREATE POLICY "Users can view own files"
ON storage.objects
FOR SELECT
TO authenticated
USING (
  bucket_id = 'uploads' AND
  (storage.foldername(name))[1] = auth.uid()::text
);

-- Policy für eigene Dateien löschen (DELETE)
CREATE POLICY "Users can delete own files"
ON storage.objects
FOR DELETE
TO authenticated
USING (
  bucket_id = 'uploads' AND
  (storage.foldername(name))[1] = auth.uid()::text
);

-- Policy für eigene Dateien aktualisieren (UPDATE)
CREATE POLICY "Users can update own files"
ON storage.objects
FOR UPDATE
TO authenticated
USING (
  bucket_id = 'uploads' AND
  (storage.foldername(name))[1] = auth.uid()::text
)
WITH CHECK (
  bucket_id = 'uploads' AND
  (storage.foldername(name))[1] = auth.uid()::text
);

Erklärung: Diese Policies erlauben es Benutzern nur, Dateien in ihren eigenen Ordnern zu verwalten (Ordnername = User ID).

Schritt 2: Frontend Implementation

React/Next.js Upload Component

Hier ist eine vollständige, production-ready Upload-Komponente:

// components/FileUpload.tsx
import { useState, useRef } from 'react'
import { createClient } from '@supabase/supabase-js'
import { useUser } from '@/hooks/useUser' // dein Auth Hook

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

interface FileUploadProps {
  onUploadComplete: (url: string) => void
  allowedTypes?: string[]
  maxSize?: number // in MB
  bucket?: string
}

export default function FileUpload({
  onUploadComplete,
  allowedTypes = ['image/jpeg', 'image/png', 'image/webp'],
  maxSize = 5,
  bucket = 'uploads'
}: FileUploadProps) {
  const [uploading, setUploading] = useState(false)
  const [uploadProgress, setUploadProgress] = useState(0)
  const [error, setError] = useState<string | null>(null)
  const fileInputRef = useRef<HTMLInputElement>(null)
  const { user } = useUser()

  const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0]
    if (!file || !user) return

    setError(null)

    // Validierung
    if (!allowedTypes.includes(file.type)) {
      setError(`Dateityp nicht erlaubt. Erlaubt: ${allowedTypes.join(', ')}`)
      return
    }

    if (file.size > maxSize * 1024 * 1024) {
      setError(`Datei zu groß. Maximum: ${maxSize}MB`)
      return
    }

    await uploadFile(file)
  }

  const uploadFile = async (file: File) => {
    setUploading(true)
    setUploadProgress(0)

    try {
      // Eindeutigen Dateinamen generieren
      const fileExt = file.name.split('.').pop()
      const fileName = `${Date.now()}-${Math.random().toString(36).substring(2)}.${fileExt}`
      const filePath = `${user.id}/${fileName}`

      // Upload mit Progress Tracking
      const { data, error } = await supabase.storage
        .from(bucket)
        .upload(filePath, file, {
          cacheControl: '3600',
          upsert: false
        })

      if (error) {
        throw error
      }

      // Public URL generieren
      const { data: { publicUrl } } = supabase.storage
        .from(bucket)
        .getPublicUrl(filePath)

      onUploadComplete(publicUrl)
      
      // Input zurücksetzen
      if (fileInputRef.current) {
        fileInputRef.current.value = ''
      }

    } catch (error: any) {
      console.error('Upload error:', error)
      setError(error.message || 'Upload fehlgeschlagen')
    } finally {
      setUploading(false)
      setUploadProgress(0)
    }
  }

  return (
    <div className="space-y-4">
      <div className="flex items-center justify-center w-full">
        <label className="flex flex-col items-center justify-center w-full h-32 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100">
          <div className="flex flex-col items-center justify-center pt-5 pb-6">
            <svg className="w-8 h-8 mb-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
            </svg>
            <p className="mb-2 text-sm text-gray-500">
              <span className="font-semibold">Klicken zum Upload</span> oder Drag & Drop
            </p>
            <p className="text-xs text-gray-500">
              {allowedTypes.join(', ')} (max. {maxSize}MB)
            </p>
          </div>
          <input
            ref={fileInputRef}
            type="file"
            className="hidden"
            onChange={handleFileSelect}
            accept={allowedTypes.join(',')}
            disabled={uploading || !user}
          />
        </label>
      </div>

      {uploading && (
        <div className="w-full bg-gray-200 rounded-full h-2.5">
          <div 
            className="bg-blue-600 h-2.5 rounded-full transition-all duration-300" 
            style={{ width: `${uploadProgress}%` }}
          ></div>
        </div>
      )}

      {error && (
        <div className="p-3 text-sm text-red-600 bg-red-50 rounded-md">
          {error}
        </div>
      )}

      {uploading && (
        <p className="text-sm text-gray-600 text-center">
          Uploading... {uploadProgress}%
        </p>
      )}
    </div>
  )
}

Usage Beispiel

// pages/profile.tsx
import FileUpload from '@/components/FileUpload'

export default function ProfilePage() {
  const handleAvatarUpload = (url: string) => {
    console.log('Avatar uploaded:', url)
    // Update user profile mit neuer Avatar URL
    updateUserProfile({ avatar_url: url })
  }

  return (
    <div>
      <h1>Profil bearbeiten</h1>
      <FileUpload
        onUploadComplete={handleAvatarUpload}
        allowedTypes={['image/jpeg', 'image/png']}
        maxSize={2}
        bucket="avatars"
      />
    </div>
  )
}

Schritt 3: Advanced Features

Progress Tracking für große Dateien

Für echtes Progress Tracking musst du den Upload chunken:

const uploadFileWithProgress = async (file: File) => {
  const chunkSize = 1024 * 1024 // 1MB chunks
  const totalChunks = Math.ceil(file.size / chunkSize)
  
  for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
    const start = chunkIndex * chunkSize
    const end = Math.min(start + chunkSize, file.size)
    const chunk = file.slice(start, end)
    
    // Upload chunk...
    const progress = ((chunkIndex + 1) / totalChunks) * 100
    setUploadProgress(progress)
  }
}

Image Resize vor Upload

const resizeImage = (file: File, maxWidth: number, maxHeight: number): Promise<File> => {
  return new Promise((resolve) => {
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')!
    const img = new Image()
    
    img.onload = () => {
      // Aspect Ratio berechnen
      const ratio = Math.min(maxWidth / img.width, maxHeight / img.height)
      canvas.width = img.width * ratio
      canvas.height = img.height * ratio
      
      // Image auf Canvas zeichnen
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
      
      // Zurück zu File konvertieren
      canvas.toBlob((blob) => {
        if (blob) {
          const resizedFile = new File([blob], file.name, {
            type: file.type,
            lastModified: Date.now()
          })
          resolve(resizedFile)
        }
      }, file.type, 0.9)
    }
    
    img.src = URL.createObjectURL(file)
  })
}

// Verwendung:
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
  const file = event.target.files?.[0]
  if (!file) return
  
  // Bild vor Upload resizen
  const resizedFile = await resizeImage(file, 1920, 1080)
  await uploadFile(resizedFile)
}

Multiple File Upload

const uploadMultipleFiles = async (files: FileList) => {
  const uploadPromises = Array.from(files).map(async (file, index) => {
    const fileExt = file.name.split('.').pop()
    const fileName = `${Date.now()}-${index}.${fileExt}`
    const filePath = `${user.id}/${fileName}`
    
    return supabase.storage
      .from('uploads')
      .upload(filePath, file)
  })
  
  try {
    const results = await Promise.all(uploadPromises)
    console.log('All files uploaded:', results)
  } catch (error) {
    console.error('Some uploads failed:', error)
  }
}

Schritt 4: Server-Side Upload (Next.js API Route)

Für sensible Uploads oder Server-Side Verarbeitung:

// pages/api/upload.ts
import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { NextApiRequest, NextApiResponse } from 'next'
import formidable from 'formidable'
import fs from 'fs'

export const config = {
  api: {
    bodyParser: false, // Disable body parsing for file uploads
  },
}

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' })
  }

  const supabase = createServerSupabaseClient({ req, res })
  
  // Check authentication
  const { data: { user }, error: authError } = await supabase.auth.getUser()
  if (!user) {
    return res.status(401).json({ error: 'Unauthorized' })
  }

  try {
    const form = formidable({
      maxFileSize: 10 * 1024 * 1024, // 10MB
      keepExtensions: true,
    })

    const [fields, files] = await form.parse(req)
    const file = Array.isArray(files.file) ? files.file[0] : files.file

    if (!file) {
      return res.status(400).json({ error: 'No file provided' })
    }

    // Read file
    const fileBuffer = fs.readFileSync(file.filepath)
    
    // Generate unique filename
    const fileExt = file.originalFilename?.split('.').pop()
    const fileName = `${Date.now()}-${Math.random().toString(36).substring(2)}.${fileExt}`
    const filePath = `${user.id}/${fileName}`

    // Upload to Supabase
    const { data, error } = await supabase.storage
      .from('uploads')
      .upload(filePath, fileBuffer, {
        contentType: file.mimetype || 'application/octet-stream',
        cacheControl: '3600'
      })

    if (error) {
      throw error
    }

    // Get public URL
    const { data: { publicUrl } } = supabase.storage
      .from('uploads')
      .getPublicUrl(filePath)

    // Clean up temp file
    fs.unlinkSync(file.filepath)

    res.status(200).json({ 
      message: 'Upload successful', 
      url: publicUrl,
      path: data.path 
    })

  } catch (error: any) {
    console.error('Upload error:', error)
    res.status(500).json({ error: error.message || 'Upload failed' })
  }
}

Schritt 5: File Management

Dateien auflisten

const getUserFiles = async () => {
  const { data, error } = await supabase.storage
    .from('uploads')
    .list(user.id, {
      limit: 100,
      offset: 0,
      sortBy: { column: 'created_at', order: 'desc' }
    })
    
  if (error) {
    console.error('Error listing files:', error)
    return []
  }
  
  return data
}

Dateien löschen

const deleteFile = async (filePath: string) => {
  const { error } = await supabase.storage
    .from('uploads')
    .remove([filePath])
    
  if (error) {
    console.error('Error deleting file:', error)
    return false
  }
  
  return true
}

Image Transformationen

Supabase bietet automatische Image-Transformationen:

// Verschiedene Größen generieren
const getImageUrl = (path: string, options?: {
  width?: number
  height?: number
  quality?: number
  format?: 'webp' | 'jpeg' | 'png'
}) => {
  const { data } = supabase.storage
    .from('uploads')
    .getPublicUrl(path, {
      transform: {
        width: options?.width,
        height: options?.height,
        quality: options?.quality,
        format: options?.format
      }
    })
    
  return data.publicUrl
}

// Usage:
const thumbnailUrl = getImageUrl('user123/image.jpg', { 
  width: 200, 
  height: 200, 
  quality: 80,
  format: 'webp'
})

Häufige Probleme und Lösungen

Problem 1: "new row violates row-level security policy"

Lösung: Überprüfe deine RLS Policies. Der User muss authentifiziert sein und die Policy muss den Upload erlauben:

-- Debug: Prüfe ob User authentifiziert ist
SELECT auth.uid(), auth.role();

-- Debug: Prüfe Bucket-Konfiguration
SELECT * FROM storage.buckets WHERE id = 'uploads';

Problem 2: Upload funktioniert, aber Bild wird nicht angezeigt

Lösung: Überprüfe die PUBLIC URL und CORS-Einstellungen:

// Korrekte URL generieren
const { data: { publicUrl }, error } = supabase.storage
  .from('uploads')
  .getPublicUrl(filePath)

if (error) {
  console.error('Error getting public URL:', error)
}

Problem 3: Langsame Uploads

Lösungen:

  1. Dateien vor Upload komprimieren
  2. Chunked Upload für große Dateien
  3. CDN aktivieren (automatisch bei Supabase)
  4. Image-Resize clientseitig

Security Best Practices

1. File Type Validation

const ALLOWED_FILE_TYPES = {
  images: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
  documents: ['application/pdf', 'application/msword', 'text/plain'],
  videos: ['video/mp4', 'video/webm', 'video/ogg']
}

const validateFileType = (file: File, category: keyof typeof ALLOWED_FILE_TYPES) => {
  return ALLOWED_FILE_TYPES[category].includes(file.type)
}

2. File Size Limits

const MAX_FILE_SIZES = {
  image: 5 * 1024 * 1024,    // 5MB
  document: 10 * 1024 * 1024, // 10MB
  video: 100 * 1024 * 1024   // 100MB
}

3. Malware Scanning

Für Production-Apps solltest du einen Malware-Scanner integrieren:

// Example mit ClamAV
const scanFile = async (fileBuffer: Buffer) => {
  // Integration mit Malware-Scanner
  // return scanResult
}

Performance Optimierung

1. Lazy Loading für File Listen

const FileList = () => {
  const [files, setFiles] = useState([])
  const [loading, setLoading] = useState(false)
  const [hasMore, setHasMore] = useState(true)

  const loadMoreFiles = async () => {
    if (loading || !hasMore) return
    
    setLoading(true)
    const newFiles = await getUserFiles(files.length)
    
    if (newFiles.length === 0) {
      setHasMore(false)
    } else {
      setFiles(prev => [...prev, ...newFiles])
    }
    
    setLoading(false)
  }

  return (
    <div>
      {files.map(file => (
        <FileItem key={file.id} file={file} />
      ))}
      {hasMore && (
        <button onClick={loadMoreFiles} disabled={loading}>
          {loading ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  )
}

2. Image Caching Strategy

// Service Worker für aggressive Caching
const cacheImages = async (urls: string[]) => {
  const cache = await caches.open('supabase-images-v1')
  await cache.addAll(urls)
}

Fazit

Supabase Storage bietet eine mächtige und flexible Lösung für File Uploads. Die wichtigsten Punkte:

  1. RLS Policies sind kritisch - ohne sie funktioniert nichts
  2. Client-side Validierung verbessert UX, aber Server-side ist Pflicht
  3. Image-Transformationen sparen Bandbreite und verbessern Performance
  4. Chunked Upload für große Dateien implementieren
  5. Security-First Approach mit File-Type und Size Validation

Mit diesen Implementierungen hast du eine production-ready File Upload Lösung, die sicher, performant und benutzerfreundlich ist.

Weitere Ressourcen

  • Supabase Storage Dokumentation
  • Storage RLS Policies Guide
  • Image Transformations

Hast du Probleme mit deinem File Upload? Schreib einen Kommentar und ich helfe gerne weiter!

Tags

  • Supabase
  • Supabase Storage

Comments

Hilfe zum Textformat

Restricted HTML

  • Erlaubte HTML-Tags: <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Zeilenumbrüche und Absätze werden automatisch erzeugt.
  • Website- und E-Mail-Adressen werden automatisch in Links umgewandelt.

Related articles

Supabase Edge Functions CORS Fehler beheben - Vollständige Anleitung 2025
Binance API Guide: Crypto Trading Bot mit Supabase Backend - Live Trading Data Storage und Monitoring

Über den Autor

Nikolai Fischer ist Gründer von Kommune3 (seit 2007) und führender Experte für die Verbindung von Software-Entwicklung und Unternehmertum. Mit 17+ Jahren Erfahrung hat er hunderte von Projekten geleitet und erreichte #1 auf Hacker News. Als Host des Podcasts "Kommit mich" und Gründer von skillution verbindet er technische Expertise mit unternehmerischem Denken. Seine Artikel über moderne Webentwicklung und systematisches Problem-Solving haben tausende von Entwicklern beeinflusst.

Folge Niko auf:

  • Website: nikofischer.com
  • LinkedIn: Nikolai Fischer
  • Podcast: Kommit mich
Ihre Anmeldung konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.
Ihre Anmeldung war erfolgreich.

Newsletter

Melden Sie sich zu unserem Newsletter an, um auf dem Laufenden zu bleiben.

Nikolai Fischer

✌ Hi, ich bin Niko
Unternehmer, Entwickler & Podcaster

Kontaktier mich:

  • E-Mail
  • Telefon
  • LinkedIn

My Reading List

  • $100M Leads: How to Get Strangers To Want To Buy Your Stuff - Alex Hormozi
  • Quantitative Trading: How to Build Your Own Algorithmic Trading Business (Wiley Trading) - Ernest P. Chan
  • Hands-On Machine Learning for Algorithmic Trading: Design and implement investment strategies based on smart algorithms that learn from data using Python - Stefan Jansen
  • Algorithmic Trading - Ernie Chan
  • Let Me Tell You a Story: Tales Along the Road to Happiness - Jorge Bucay
more
RSS feed