DSGVO-Recht auf Löschung: Nutzer aus komplexen SaaS-Systemen tatsächlich entfernen
Ein B2B-SaaS-Team, das ich letztes Quartal reviewte, hatte einen "DSGVO - Mein Konto löschen"-Button in ihrem Produkt. Ein Klick darauf setzte users.deleted_at auf den aktuellen Timestamp und schickte eine E-Mail an den Support. Das war alles. Audit-Logs speicherten weiterhin vollständige Namen und E-Mail-Adressen. Ihr nächtliches Backup behielt jeden gelöschten Datensatz 90 Tage lang. Das Data Warehouse verfolgte weiterhin die Verhaltens-Events des Nutzers, verschlüsselt mit einer user_id, die nie verschwand. Als ein Kunde schließlich einen formellen Artikel-17-Antrag stellte und um Löschungsnachweis bat, brauchte das Team drei Ingenieure und vier Tage, um etwas Vertretbares zu produzieren - und die Antwort war noch immer "größtenteils ja, mit Vorbehalten."
Das ist die Lücke zwischen DSGVO-Recht auf Löschung auf dem Papier und dem Recht auf Löschung im Produktionsbetrieb. Der Rechtstext ist kurz, die Engineering-Oberfläche ist enorm, und die meisten Teams entdecken den zweiten Teil erst, wenn eine Datenschutzbehörde einen höflichen Brief schickt oder ein Sicherheitsfragebogen eines Enterprise-Interessenten auf dem Schreibtisch landet. Dieser Beitrag zeigt, wie eine realistische Lösch-Pipeline in modernen SaaS-Architekturen tatsächlich aussieht: der Inventarisierungsschritt, den niemand macht, das Backup-Problem ohne saubere Lösung, das Tombstone-Muster für Foreign Keys und die Orchestrierungslogik, die Multi-System-Löschungen unter Fehlerbedingungen ehrlich hält.
Was "Löschung" gemäß Artikel 17 tatsächlich bedeutet
Artikel 17 DSGVO gewährt betroffenen Personen das Recht, die Löschung ihrer personenbezogenen Daten "unverzüglich" zu erwirken - in der Praxis als innerhalb eines Monats interpretiert, verlängerbar um zwei weitere bei komplexen Anfragen. Löschung bedeutet, dass die Daten nicht mehr verarbeitbar sind, nicht unbedingt dass jedes Byte auf jeder Festplatte in jeder Jurisdiktion überschrieben wurde. Die Leitlinien des Europäischen Datenschutzausschusses akzeptieren, dass "Daten außer Reichweite bringen" - auf eine Weise, die irreversibel ist, nicht in normalen Betriebsabläufen zugänglich ist und am Ende eines bestehenden Aufbewahrungszyklus sicher vernichtet wird - Artikel 17 für Backup-Tapes und ähnliche Systeme erfüllen kann.
Zwei Konsequenzen sind für Ingenieure wichtig. Erstens bist du nicht verpflichtet, deine Backup-Architektur zu brechen, um eine Löschanfrage am Tag ihres Eintreffens zu erfüllen; du bist verpflichtet sicherzustellen, dass der gelöschte Nutzer nach einer Wiederherstellung nicht in der Produktion wieder auftaucht. Zweitens kannst du diese Flexibilität nicht als Pauschalentschuldigung nutzen: alles, was über normale Anwendungspfade erreichbar ist - primäre Datenbank, Suchindex, Cache, Analytics-Warehouse, Audit-Log-Viewer, exportierte Berichte - muss ordnungsgemäß und termingerecht gelöscht werden, mit dokumentierten Nachweisen.
Es gibt auch Ausnahmen: Artikel 17(3) erlaubt die Aufbewahrung von Daten, die für Rechtsansprüche, die Buchführung (typischerweise zehn Jahre in Deutschland gemäß HGB §257), öffentliche Archive oder Meinungsfreiheit erforderlich sind. Ein "Recht auf Vergessenwerden"-Antrag eines Nutzers, der dir noch Geld schuldet, löscht nicht den Rechnungspfad.
Schritt eins: Inventarisieren, wo personenbezogene Daten tatsächlich leben
Die meisten Löschfehler beginnen mit einem veralteten Dateninventar. Ein typisches Symfony + React SaaS bei 5-20 Mio. EUR ARR wird personenbezogene Daten mindestens an folgenden Stellen haben:
- Die primäre relationale Datenbank (
users,accounts,team_members,invitations,audit_log,notifications,support_messages) - Ein Suchindex (Meilisearch, Typesense, Elasticsearch) mit einer denormalisierten Ansicht von Nutzern und ihren Inhalten
- Ein oder mehrere Caches (Redis, Memcached, In-Process-LRU) mit nutzergebundenen Payloads
- Object Storage (S3, MinIO, Backblaze B2) für Uploads, Exports, Profilbilder, generierte PDFs
- E-Mail- und Transaktionsanbieter (SendGrid, Postmark, Mailgun) mit Webhook-Archiven und Nachrichtenlogs
- Ein Data Warehouse (BigQuery, Snowflake, Postgres + dbt), das Nutzer, Events und Abrechnungsdaten repliziert
- Produkt-Analytics (Amplitude, Mixpanel, PostHog), verschlüsselt nach
user_idoder Geräte-ID - CRM und Support-Tools (HubSpot, Salesforce, Zendesk) mit Notizen und Ticket-Historien
- Backups, Snapshots und Replikate jeder der obigen Datenbanken
Bevor du einen Lösch-Endpoint schreibst, führe eine Datenmapping-Übung durch und schreibe das Ergebnis auf. Eine einfache Tabelle mit Spalten für System, Speicherort, Identifikator, Aufbewahrungsgrundlage, Löschmechanismus reicht aus. Das Inventar ist die Quelle der Wahrheit für deine Lösch-Pipeline. Das Überspringen des Inventars ist die häufigste Ursache für "Wir haben vergessen, dass wir PII dort hatten"-Vorfälle - typischerweise während eines Code-Qualitäts-Audits oder einer Beschaffungs-Sicherheitsprüfung entdeckt.
Das Backup-Problem und warum du es nicht chirurgisch lösen solltest
Die am häufigsten gestellte Frage ist: "Muss ich den Nutzer aus jedem nächtlichen Datenbank-Backup löschen?" Chirurgisches Löschen aus binären Datenbank-Backups ist technisch möglich, operativ erschreckend und fast nie die richtige Antwort. Backups wiederherstellen, mutieren, neu verschlüsseln und für jede Löschanfrage erneut hochladen schafft mehr Risiken als es beseitigt.
Der akzeptierte Ansatz ist Löschung-per-Replay. Du speicherst Löschanfragen in einem Append-Only-Log in der Produktion, getrennt von den primären Tabellen. Wenn ein Backup wiederhergestellt wird, ist der allererste Schritt des Wiederherstellungs-Runbooks, das Löschlog gegen die frisch wiederhergestellte Datenbank auszuführen und jeden Nutzer zu löschen, der zwischen dem Backup-Datum und dem Wiederherstellungsdatum zur Löschung markiert wurde.
// src/Entity/ErasureRequest.php
#[ORM\Entity]
#[ORM\Table(name: 'erasure_requests')]
class ErasureRequest
{
#[ORM\Id]
#[ORM\Column(type: 'uuid')]
public string $id;
#[ORM\Column(type: 'string', length: 64)]
public string $subjectHash; // SHA-256 des kanonischen Nutzer-Identifikators
#[ORM\Column(type: 'datetimetz_immutable')]
public \DateTimeImmutable $requestedAt;
#[ORM\Column(type: 'datetimetz_immutable', nullable: true)]
public ?\DateTimeImmutable $completedAt = null;
#[ORM\Column(type: 'string', length: 32)]
public string $status; // received | in_progress | completed | failed
}
Das Log speichert einen Hash des Nutzer-Identifikators, nicht die E-Mail oder Nutzer-ID selbst. Damit kann das Löschlog die gelöschten Daten überleben ohne erneut PII einzuführen. Deine Backup-Aufbewahrungsrichtlinie muss das Löschlog explizit abdecken.
Tombstones, Foreign Keys und die "Wir können nicht einfach DELETE" Realität
In einem echten Schema bricht das Hard-Delete eines Nutzers Invarianten überall. Ihre Rechnungen, Audit-Log-Einträge, Support-Tickets, Kommentare und Referenzen sind noch rechtlich bedeutsam - die Transaktionsfakten müssen bleiben, auch wenn die personenbezogenen Daten entfernt werden. Das Muster, das im Produktionsbetrieb funktioniert, ist der Tombstone: der Nutzer-Datensatz wird an Ort und Stelle anonymisiert, Foreign Keys bleiben gültig und abhängige Tabellen haben ihre PII-Spalten genullt oder gehasht.
BEGIN;
-- Primären Nutzerdatensatz anonymisieren, aber Zeile behalten
UPDATE users
SET email = CONCAT('erased+', id, '@example.invalid'),
full_name = 'Gelöschter Nutzer',
avatar_url = NULL,
locale = NULL,
last_login_ip = NULL,
erased_at = NOW(),
status = 'erased'
WHERE id = $1;
-- PII aus abhängigen Tabellen entfernen, Transaktionsstruktur behalten
UPDATE audit_log
SET actor_email = NULL, ip_address = NULL, user_agent = NULL
WHERE actor_id = $1;
UPDATE support_messages
SET body = '[Inhalt gelöscht]', attachment_url = NULL
WHERE author_id = $1;
-- Datensätze ohne legitime Aufbewahrungsgrundlage hard-deleten
DELETE FROM api_tokens WHERE user_id = $1;
DELETE FROM webhook_subscriptions WHERE user_id = $1;
DELETE FROM notification_inbox WHERE user_id = $1;
COMMIT;
Verwende eine Fake-Top-Level-Domain wie .invalid (RFC 6761), damit niemand versehentlich einen gelöschten Nutzer anschreibt. Füge einen CI-Test hinzu, der behauptet, dass neue Tabellen mit nutzergebundenen Spalten auch eine entsprechende Klausel in der Löschroutine haben. Wir fügen diese Art von Absicherung regelmäßig während Legacy-Code-Optimierungs-Projekten hinzu.
Multi-System-Orchestrierung: Saga-Muster für Löschungen
Löschung über das Warehouse, den Suchindex, den E-Mail-Anbieter und deine eigene Datenbank hinweg ist keine einzelne Transaktion. Es ist ein verteilter Workflow, der partielle Fehler ehrlich behandeln muss: HubSpots API kann Rate-Limits haben, Snowflake ist asynchron, Meilisearch reindiziert lazy. Behandle Löschung wie jeden anderen langläufigen Geschäftsprozess - mit einem Saga, einem State Machine und idempotenten Schritten.
// src/Service/Erasure/ErasurePipeline.php
final class ErasurePipeline
{
public function __construct(
private readonly UserAnonymizer $primaryDb,
private readonly SearchIndexCleaner $search,
private readonly WarehouseRedactor $warehouse,
private readonly EmailProviderCleaner $email,
private readonly StorageCleaner $storage,
private readonly ErasureRequestRepository $requests,
) {}
public function run(string $requestId): void
{
$request = $this->requests->lockForUpdate($requestId);
$steps = [
'primary_db' => fn() => $this->primaryDb->anonymise($request),
'search' => fn() => $this->search->purge($request),
'storage' => fn() => $this->storage->deleteObjects($request),
'email' => fn() => $this->email->suppressAndPurge($request),
'warehouse' => fn() => $this->warehouse->scheduleRedaction($request),
];
foreach ($steps as $name => $fn) {
if ($request->isStepDone($name)) continue;
try {
$fn();
$request->markStepDone($name);
$this->requests->save($request);
} catch (TransientException $e) {
$this->requests->scheduleRetry($request, $name, $e);
return;
}
}
$request->status = 'completed';
$request->completedAt = new \DateTimeImmutable();
$this->requests->save($request);
}
}
Drei Eigenschaften sind wichtiger als das spezifische Framework. Jeder Schritt muss idempotent sein. Jeder Schritt muss den Request-Status aktualisieren bevor er zurückkehrt. Und der Orchestrator muss transiente Fehler (Rate-Limits, Netzwerk-Blips) von permanenten Fehlern unterscheiden.
Für das Warehouse ist Echtzeit-Löschung selten möglich. Der pragmatische Ansatz ist, einen täglichen DAG zu pflegen, der Faktentabellen gegen das Löschlog joinnt und entweder übereinstimmende Zeilen löscht oder PII-Spalten mit Hash-Werten ersetzt.
Was dem Nutzer und dem Datenschutzbeauftragten übergeben werden sollte
Das Schließen des Loops ist Teil der Verpflichtung. Die betroffene Person hat Anspruch auf Bestätigung, dass ihre Daten gelöscht wurden, und dein Datenschutzbeauftragter hat Anspruch auf Belege, dass das System funktioniert hat. Das richtige Ergebnis ist kein Screenshot - es ist ein generiertes Zertifikat, das zusammenfasst, welche Systeme verarbeitet wurden, wann jeder Schritt abgeschlossen wurde, welche Datenkategorien aus rechtlichen Gründen verblieben sind und das Aufbewahrungsende für diese Kategorien.
Für B2B-SaaS wird das zu einem Verkaufsargument. Enterprise-Beschaffungsteams fragen routinemäßig nach einem Nachweispaket zur Artikel-17-Implementierung; Teams, die ein einseitiges Architekturdokument und ein Musterzertifikat übergeben können, sind Wochen vor Wettbewerbern, die "Ja, wir erfüllen" antworten.
Eine realistische Checkliste
Führe deine aktuelle Implementierung gegen diese Liste aus und notiere, wo sie versagt. Die Lücken sagen dir, was als nächstes zu bauen ist.
Das Dateninventar existiert, ist aktuell und wird bei jeder Schema-Änderung überprüft. Das Löschlog ist Append-Only, speichert gehashte Identifikatoren und wird bei jeder Backup-Wiederherstellung wiedergegeben. Die primäre Datenbankl öschung ist eine Tombstone-Operation, die jede PII-Spalte in jeder vom Nutzer berührten Tabelle abdeckt. Suchindex, Cache, Object Storage und E-Mail-Anbieter werden alle von benannten, idempotenten Schritten in einem Saga behandelt. Das Data Warehouse hat einen täglichen Redaktions-DAG, der vom Löschlog gesteuert wird. Fehler sind beobachtbar, werden mit Backoff wiederholt und erzeugen Alerts nach einem konfigurierten Schwellenwert. Eine betroffene Person kann ein Löschzertifikat anfordern und erhalten. Aufbewahrte Daten haben eine dokumentierte rechtliche Grundlage und ein Löschdatum.
DSGVO-Recht auf Löschung ist lösbar, aber nur, wenn du es als echte Engineering-Oberfläche behandelst - mit einem Schema, einem Workflow, Tests und Observability - anstatt als Compliance-Checkbox an einem CRUD-Endpoint.
Wenn du auf einen halb implementierten Lösch-Flow und eine vierteljährliche Datenschutzbeauftragte-Überprüfung im Kalender schaust, können wir helfen. Kontaktiere uns unter hello@wolf-tech.io oder besuche wolf-tech.io für eine kostenlose Beratung zu DSGVO-konformem Datenlöschungs-Engineering für europäische SaaS.

