Worum es geht: dsgvoschulfotos.de ist ein soziales Projekt: DSGVO-konformes Foto-Sharing für Schulen und Kitas — Eltern greifen per QR-Code und PIN zu, ganz ohne Account. Die Sicherheitsarchitektur (private Buckets, Row Level Security, signierte URLs) ist solide. Aber jede dieser Entscheidungen hat einen Performance-Preis, und nicht jede unserer Entscheidungen war gut. Dieser Artikel zeigt beides — mit echten Produktionszahlen.

Die Architektur: bewusst klein, bewusst geschlossen
Die Plattform läuft komplett auf einem Hetzner-Server mit 2 vCPU und 8 GB RAM: Next.js 16 mit React 19, ein selbst gehostetes Supabase mit 16–20 Docker-Containern (darunter Kong als API-Gateway, die Storage-API und imgproxy), PostgreSQL mit Row Level Security, die Fotos in einem privaten Hetzner-S3-Bucket. Kein externes SaaS, keine US-Cloud — bei Kinderfotos ist das kein Nice-to-have, sondern die Geschäftsgrundlage.
- Kein Eltern-Account. Zugriff über einen QR-Code plus PIN, der einen kindbezogenen Token freischaltet. Keine Passwörter, keine E-Mail-Verteiler, kein Identitäts-Datenbestand, der leaken könnte.
- Row Level Security (RLS) in PostgreSQL. Die Datenbank selbst erzwingt, wer welche Zeilen sehen darf — nicht nur der Anwendungscode.
- Privater Storage, signierte URLs. Kein Foto ist öffentlich erreichbar. Jede Bild-URL wird von der Storage-API kryptografisch signiert und läuft nach 24 Stunden ab.
- Audit-Log. Sicherheitsrelevante Aktionen werden protokolliert und sind für Administratoren mit serverseitiger Pagination, Filtern und Volltextsuche einsehbar.
Der Preis der Sicherheit, Teil 1: Thumbnails, die sich zu Tode rechnen
Privater Storage heißt: kein simples CDN davor, keine öffentlichen Bild-URLs. Der ältere Schul-Workflow generierte deshalb Thumbnails beim Upload vor. Der neuere Fotografen-Workflow tat das nicht — er speicherte nur Originale, und jede Thumbnail-Anfrage war eine On-the-fly-Transformation eines Full-Resolution-Originals durch imgproxy. Auf 2 CPU-Kernen.
Das erste echte Fotografen-Album (1.246 Fotos, 944 MB) machte daraus einen messbaren Ausfall. Kundenmeldung, wörtliche Kategorie: „Thumbnails nicht geladen". Die Produktions-Logs zeigten 723 imgproxy-Fehler in 72 Stunden in zwei Varianten:
Request was timed out after 10s— die Worker-Queue von imgproxy war gesättigt (HTTP 503)Can't download source image— der 5-Sekunden-Timeout beim S3-Download eines Multi-MB-Originals (HTTP 500)
Eine Grid-Ansicht feuert Dutzende solcher Anfragen gleichzeitig. Jede dekodiert ein JPEG von über 1 MB, skaliert es und enkodiert neu. Zwei Kerne. Diese Rechnung konnte nie aufgehen — sie ist nur bei 50-Foto-Testalben nie fällig geworden.
Der Fix: drei WebP-Varianten pro Foto, vorgeneriert
| Variante | Breite | Qualität | Typische Größe | Einsatz |
|---|---|---|---|---|
| thumb | 300 px | 80 | ~32–47 KB (gemessen) | Galerie-Grid |
| cover | 800 px | 80 | ~100–200 KB | Karten, Vorschau |
| full | 1.920 px | 85 | ~300–500 KB | Lightbox |
Der Backfill für den Bestand, gemessen auf Produktion: 3.101 Fotos verarbeitet, 9.180 WebP-Objekte mit 1,668 GiB nach S3 geschrieben. 9 dauerhafte Fehler — alle waren verwaiste Test-Datensätze, deren S3-Objekte längst gelöscht waren. Die Fehlerliste fand also Datenleichen, keine Code-Bugs.
Das Ergebnis: Eine signierte Thumbnail-URL lieferte nach dem Backfill HTTP 200 mit 32 KB WebP in 82 ms — vorher liefen dieselben Anfragen in 10-Sekunden-Timeouts. Die imgproxy-Fehlerrate in den 30 Minuten nach dem Backfill: 0. imgproxy bleibt nur noch als Fallback für Fotos, deren Varianten der 15-Minuten-Cron noch nicht erzeugt hat.
Der Preis der Sicherheit, Teil 2: signierte URLs gegen den Browser-Cache
Die zweite versteckte Rechnung: Signierte URLs enthalten ein Ablauf-Token, das bei jedem Seitenaufruf neu generiert wird. Identisches Bild, anderer URL-String — für den Browser-Cache sind das zwei verschiedene Ressourcen. Jeder Reload lädt jedes Thumbnail erneut herunter, und ein CDN könnte ebenfalls nichts wiederverwenden.
Wir haben das bewusst noch nicht gefixt und dokumentieren es als offene Schuld: Der geplante Fix ist ein serverseitiger URL-Cache (In-Memory-Map, Schlüssel photoId:variant, TTL kürzer als die 24-h-Gültigkeit), damit wiederholte Seitenaufrufe byte-identische URLs erhalten und der HTTP-Cache wieder greift. Wer eine Signed-URL-Architektur plant, sollte diesen Posten von Anfang an einkalkulieren.
Nicht jede Entscheidung war gut — drei ehrliche Befunde
- Der neue Workflow vergaß die Lektion des alten. Der Schul-Flow hatte vorgenerierte Thumbnails, der später gebaute Fotografen-Flow nicht. Dieselbe Firma, dieselbe Codebasis, dieselbe Falle — zweimal.
- Die Admin-Tür führte am Fix vorbei. Die Superadmin-Ansicht forderte weiterhin Echtzeit-Transformationen der Originale an (800 px und 1.600 px) und umging die Varianten komplett — dasselbe Problem, durch die Hintertür wieder eingebaut.
- Farbpunkte als einziges UI-Signal skalieren nicht. Die Foto-Zuordnung markierte Kinder mit farbigen Punkten: 12 Farben in der Palette, 13+ Kinder in einer Kita-Gruppe. Ab Foto Nummer 400 einer Abend-Session erinnert sich niemand an eine 13-Farben-Legende. Der Fix — Namens-Pills mit eigenem ✕ pro Kind statt Farbcodes — brauchte null Änderungen am Datenmodell. UX-Schulden und Schema-Schulden sind verschiedene Schulden.
Verifikation auf Produktion statt Bauchgefühl
Jede Zahl in diesem Artikel stammt aus Messungen vom 10.–12. Juni 2026: PostgreSQL-Abfragen direkt auf der Produktionsdatenbank, curl-Timings inklusive TLS von einem externen Client, Container-Logs für die imgproxy-Fehlerraten. Für den Volltest wurde ein temporäres Album-Token eingesetzt und nach der Messung gelöscht; der PIN-Flow der Eltern wurde separat durchgemessen (26 Fotos in 0,31 s). Details und alle Vorher-nachher-Werte stehen im ersten Teil der Serie: Case Study: 1.200 Fotos pro Album — wie 2.408 versteckte Requests eine Galerie ausbremsten.
Die Checkliste: Sicherheit planen heißt Performance mitplanen
- Privater Storage? Dann Bildvarianten beim Upload vorgenerieren — Echtzeit-Transformationen sind ein Ausfall mit Ansage.
- Signierte URLs? Batch-Signierung nutzen und URL-Erzeugung pro TTL-Fenster deterministisch machen, sonst stirbt das Caching.
- Token statt Accounts? Gut für den Datenschutz — aber jeden Zugriffspfad (auch Admin-Ansichten!) auf dieselben optimierten Pfade zwingen.
- Kleiner Server? Völlig legitim. Aber dann mit realistischen Datenmengen testen: 50-Foto-Testalben verstecken jedes Skalierungsproblem.
- Ehrlich dokumentieren, was offen ist. Eine bekannte, dokumentierte Schwäche ist handhabbar — eine unbekannte meldet sich als Kundenbeschwerde.
Wie sich Ladezeit direkt auf Anfragen und Umsatz auswirkt, zeigt unser Grundlagen-Artikel Warum langsame Websites Sie jeden Tag Kunden kosten.
Häufige Fragen
Wie funktioniert eine DSGVO-konforme Fotoplattform ohne Eltern-Accounts?
Über zugriffsbeschränkte Token: Eltern erhalten einen QR-Code plus PIN, der nur die Fotos des eigenen Kindes freischaltet (kindbezogener Token mit Session). Es gibt keine Accounts, keine Passwörter und keine E-Mail-Adressen der Eltern im System. Die Fotos liegen in einem privaten Bucket, jede Bild-URL ist signiert und läuft nach 24 Stunden ab.
Warum sind On-the-fly-Bildtransformationen bei großen Alben riskant?
Weil jede Thumbnail-Anfrage ein Original dekodieren, skalieren und neu enkodieren muss. Auf einem 2-vCPU-Server führte das bei einem 1.246-Foto-Album zu 723 imgproxy-Fehlern in 72 Stunden (10-s-Timeouts und abgebrochene S3-Downloads). Vorgenerierte WebP-Varianten lieferten dasselbe Thumbnail danach in 82 ms.
Welche Bildvarianten sollte man vorgenerieren?
In diesem Projekt drei WebP-Varianten pro Foto: thumb (300 px, Qualität 80, ~32–47 KB) für Grids, cover (800 px, ~100–200 KB) für Karten und Vorschauen, full (1920 px, Qualität 85, ~300–500 KB) für die Lightbox. Der Backfill für 3.101 Bestandsfotos erzeugte 9.180 WebP-Objekte mit 1,668 GiB.
Machen signierte URLs eine Website langsamer?
Indirekt ja, auf zwei Wegen: Erstens kostet jede Signierung einen internen HTTP-Request — bei Einzelsignierung pro Foto entsteht ein N+1-Problem. Zweitens wird das Ablauf-Token bei jedem Seitenaufruf neu generiert, wodurch Browser- und CDN-Cache strukturell leerlaufen: identisches Bild, anderer URL-String, erneuter Download.
Quellen & Werkzeuge
- Produktions-Logs und -Messungen dsgvoschulfotos.de, 10.–12. Juni 2026 (imgproxy-/Logflare-Logs, PostgreSQL, curl-Timings)
- Supabase: Row Level Security in PostgreSQL
- imgproxy — On-the-fly-Bildtransformation
- Google Developers: WebP-Bildformat
- dsgvoschulfotos.de — DSGVO-konforme Schulfotos
