TS
Thomas Schmitz

Freiberuflicher IT-Berater, Softwareentwickler & DevOps. Praxisnahe Artikel zu API-Design, Architektur und Cloud-Workflows.

Serie: API-Design · Teil 21 von 22

API-Design Teil 21: Datenschutz & Datenlebenszyklus

Privacy by Design – von Data Minimization über Retention Policies bis zur Umsetzung von DSGVO-Betroffenenrechten in APIs.

Praxisnah Checkliste

Datenschutz ist kein Feature, das man nachträglich einbaut. Es ist ein Design-Constraint, der von Anfang an mitgedacht werden muss. APIs, die personenbezogene Daten verarbeiten, müssen die Rechte der Betroffenen respektieren – nicht nur aus rechtlichen Gründen, sondern weil es das Richtige ist.

Zielbild

Nach diesem Artikel kannst du:

  • Data Minimization als Design-Prinzip anwenden
  • Retention Policies für verschiedene Datentypen definieren
  • DSGVO-Betroffenenrechte (Auskunft, Löschung, Export) in APIs umsetzen
  • PII in Logs und Non-Prod-Umgebungen schützen

Kernfragen

  1. Welche Daten dürfen wir speichern, wie lange, wo?
  2. Wie unterstützen wir DSGVO-Anfragen?
  3. Wie schützen wir PII außerhalb von Production?

Data Minimization

Das Prinzip der Datensparsamkeit: Nur erheben, was wirklich benötigt wird.

Entscheidungsframework

NEIN

JA

JA

NEIN

Brauchen wir es für die Kernfunktion?

Nicht erheben

Können wir es anonymisieren/pseudonymisieren?

Anonymisiert speichern

Wie lange brauchen wir es?

Retention Policy definieren
Automatische Löschung einrichten

Wer braucht Zugriff?
• Minimale Berechtigungen (Least Privilege)
• Zugriff protokollieren (Audit Log)

Beispiel: User-Registrierung

Feld Notwendig? Entscheidung
Email Ja Speichern (Login)
Passwort-Hash Ja Speichern (Auth)
Name Ja Speichern (Anzeige)
Geburtsdatum Nein* Nicht erheben
Telefon Optional Nur wenn 2FA gewünscht
Adresse Nein* Erst bei Bestellung
IP-Adresse Temporär Log, 7–30 Tage Retention
User-Agent Temporär Log, 7–30 Tage Retention

Hinweis: außer bei gesetzlicher Pflicht (z. B. Altersprüfung)

API-Design für Datensparsamkeit

// SCHLECHT: Zu viele Daten auf einmal erfragen
interface BadRegistrationRequest {
    email: string;
    password: string;
    first_name: string;
    last_name: string;
    date_of_birth: string;    // Nicht nötig für Registrierung
    phone: string;            // Nicht nötig für Registrierung
    address: Address;         // Nicht nötig für Registrierung
    marketing_consent: boolean;
}

// GUT: Nur das Nötigste, Rest später bei Bedarf
interface RegistrationRequest {
    email: string;
    password: string;
    display_name: string;
}

// Später, wenn Adresse benötigt wird (z.B. Checkout)
interface AddShippingAddressRequest {
    order_id: string;         // Kontext: Wofür wird Adresse gebraucht
    address: Address;
}

Retention Policies

Daten haben ein Verfallsdatum. Retention Policies definieren, wann was gelöscht wird. Aufbewahrungsfristen hängen von Rechtsraum und Branche ab. In Deutschland gelten je nach Dokumenttyp häufig 6/8/10 Jahre (z. B. HGB § 257, AO § 147).

Retention-Matrix

Datentyp Retention Begründung Löschmethode
Account-Daten Bis Zweckende + Inaktivitätsfrist Vertrag/legitimes Interesse Hard Delete
Buchungsbelege/Rechnungen (DE) 8–10 Jahre HGB § 257, AO § 147 Archivierung/Anonymisieren
Handels-/Geschäftsbriefe (DE) 6 Jahre HGB § 257, AO § 147 Archivierung
Audit Logs 6–12 Monate (Beispiel) Security/Compliance Rotation
Access Logs 7–30 Tage (Beispiel) Security/Debugging Rotation
Session Tokens 24h / 30d Security Auto-Expire
Password Reset 1 Stunde Security Auto-Expire
Gelöschte Accounts (Self-Service) 14–30 Tage Undo-Option Hard Delete
Analytics (PII) 0 Privacy Aggregieren/Anonymisieren
Analytics (anon) 12–24 Monate Business Aggregierung/Archivierung

Implementierung: Soft Delete mit Retention

Soft Delete mit Grace Period eignet sich für selbst initiierte Löschungen mit Undo. Bei DSGVO-Löschanfragen gilt: ohne unangemessene Verzögerung löschen, und nur dort aufbewahren, wo eine rechtliche Pflicht besteht.

// Entity mit Soft Delete
@Entity()
class User {
    @PrimaryGeneratedColumn('uuid')
    id: string;

    @Column()
    email: string;

    @Column({nullable: true})
    deletedAt: Date | null;

    @Column({nullable: true})
    scheduledPurgeAt: Date | null;  // Endgültige Löschung
}

// Lösch-Service
class UserDeletionService {
    async softDelete(userId: string): Promise<void> {
        const now = new Date();
        const purgeDate = addDays(now, 30);  // 30 Tage Grace Period (Self-Service)
        const anonymizedEmail = `deleted-${crypto.randomUUID()}@example.invalid`;  // z. B. randomUUID()

        // Benachrichtigung vor Anonymisierung (falls wiederherstellbar)
        await this.notificationService.sendDeletionConfirmation(userId);

        await this.userRepository.update(userId, {
            deletedAt: now,
            scheduledPurgeAt: purgeDate,
            // PII sofort pseudonymisieren (Unique-Constraints beachten)
            email: anonymizedEmail,
            displayName: 'Deleted User',
        });
    }

    // Scheduled Job: Endgültige Löschung
    @Cron('0 2 * * *')  // Täglich um 02:00
    async purgeExpiredUsers(): Promise<void> {
        const users = await this.userRepository.find({
            where: {
                scheduledPurgeAt: LessThan(new Date()),
                deletedAt: Not(IsNull()),
            },
        });

        for (const user of users) {
            await this.hardDelete(user.id);
        }
    }

    private async hardDelete(userId: string): Promise<void> {
        // Reihenfolge beachten wegen Foreign Keys
        await this.auditLogRepository.anonymize({userId});
        await this.orderRepository.anonymize({userId});
        await this.userRepository.delete(userId);

        this.logger.info('User purged', {userId, reason: 'retention_policy'});
    }
}

Automatische Log-Retention

# Elasticsearch ILM Policy
PUT _ilm/policy/api-logs
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_size": "50GB",
            "max_age": "1d"
          }
        }
      },
      "warm": {
        "min_age": "7d",
        "actions": {
          "shrink": { "number_of_shards": 1 },
          "forcemerge": { "max_num_segments": 1 }
        }
      },
      "delete": {
        "min_age": "90d",
        "actions": {
          "delete": { }
        }
      }
    }
  }
}

DSGVO-Betroffenenrechte

Die DSGVO definiert Rechte, die APIs aktiv unterstützen müssen. Antworten müssen grundsätzlich innerhalb eines Monats erfolgen; bei komplexen oder zahlreichen Anfragen ist eine Verlängerung um bis zu zwei weitere Monate möglich (mit Begründung).

Übersicht der Rechte

Artikel Recht Beschreibung API
Art. 15 Auskunftsrecht Betroffene können Auskunft über ihre Daten verlangen. GET /users/{id}/data-export
Art. 17 Recht auf Löschung (Vergessenwerden) Betroffene können Löschung ihrer Daten verlangen. DELETE /users/{id} oder POST /users/{id}/deletion
Art. 20 Recht auf Datenübertragbarkeit Daten in maschinenlesbarem Format exportieren. GET /users/{id}/data-export?format=json
Art. 16 Recht auf Berichtigung Unrichtige Daten korrigieren lassen. PATCH /users/{id}
Art. 21 Widerspruchsrecht Verarbeitung zu bestimmten Zwecken widersprechen. PATCH /users/{id}/consents

Hinweis: Art. 20 (Datenübertragbarkeit) gilt nur für Daten, die die betroffene Person bereitgestellt hat, die auf Einwilligung oder Vertrag beruhen und automatisiert verarbeitet werden. Abgeleitete/analytische Daten sind davon ausgenommen. Art. 15 (Auskunft) ist in der Regel weiter gefasst.

Implementierung: Datenexport (Art. 15 & 20)

// Controller
@Controller('users')
class UserDataController {
    @Get(':id/data-export')
    @UseGuards(AuthGuard, SameUserGuard)  // Nur eigene Daten
    async exportData(
        @Param('id') userId: string,
        @Query('format') format: 'json' | 'csv' = 'json',
        @Query('scope') scope: 'access' | 'portability' = 'access'
    ): Promise<DataExportResponse> {
        // Async Job starten (kann bei vielen Daten dauern)
        const exportJob = await this.dataExportService.startExport(userId, format, scope);

        return {
            jobId: exportJob.id,
            status: 'processing',
            estimatedCompletion: exportJob.estimatedCompletion,
            downloadUrl: null,  // Wird nach Fertigstellung gesetzt
        };
    }

    @Get(':id/data-export/:jobId')
    @UseGuards(AuthGuard, SameUserGuard)
    async getExportStatus(
        @Param('id') userId: string,
        @Param('jobId') jobId: string
    ): Promise<DataExportResponse> {
        const job = await this.dataExportService.getJob(jobId, userId);

        if (job.status === 'completed') {
            return {
                jobId: job.id,
                status: 'completed',
                downloadUrl: job.downloadUrl,  // Signed URL, 24h gültig
                expiresAt: job.downloadExpiresAt,
            };
        }

        return {
            jobId: job.id,
            status: job.status,
            estimatedCompletion: job.estimatedCompletion,
        };
    }
}

// Service
class DataExportService {
    async generateExport(
        userId: string,
        format: string,
        scope: 'access' | 'portability'
    ): Promise<Buffer> {
        // Alle Daten des Users sammeln
        const userData = {
            profile: await this.userRepository.findById(userId),
            orders: await this.orderRepository.findByUser(userId),
            preferences: await this.preferenceRepository.findByUser(userId),
            consents: await this.consentRepository.findByUser(userId),
            activityLog: await this.activityRepository.findByUser(userId, {
                limit: 1000,  // Letzte 1000 Aktivitäten
            }),
        };

        // Interne IDs und sensible Felder entfernen
        const sanitized = this.sanitizeForExport(userData);
        const payload = scope === 'portability'
            ? this.filterPortableData(sanitized)
            : sanitized;

        if (format === 'json') {
            return Buffer.from(JSON.stringify(payload, null, 2));
        }

        return this.convertToCsv(payload);
    }

    private sanitizeForExport(data: any): any {
        // Entfernen: interne IDs, Passwort-Hashes, etc.
        const {passwordHash, internalNotes, ...safe} = data.profile;
        return {
            ...data,
            profile: safe,
            exportedAt: new Date().toISOString(),
            exportFormat: 'GDPR Article 15/20 Data Export',
        };
    }

    private filterPortableData(data: any): any {
        // Nur Daten, die die Person bereitgestellt hat bzw. beobachtet wurden
        return {
            profile: data.profile,
            orders: data.orders,
            preferences: data.preferences,
            consents: data.consents,
            activityLog: data.activityLog,
        };
    }
}

Implementierung: Löschung (Art. 17)

Wichtig: Aufbewahrungspflichten blockieren den Vorgang nicht komplett. Du löschst alles, was gelöscht werden darf, anonymisierst den Rest und informierst die betroffene Person transparent.

// Controller
@Controller('users')
class UserDeletionController {
    @Post(':id/deletion-request')
    @UseGuards(AuthGuard, SameUserGuard)
    async requestDeletion(
        @Param('id') userId: string,
        @Body() request: DeletionRequest
    ): Promise<DeletionResponse> {
        // Prüfen ob Löschung möglich (z.B. offene Bestellungen)
        const {blockers, retentionNotice} = await this.deletionService.checkBlockers(userId);

        if (blockers.length > 0) {
            throw new ConflictException({
                type: 'https://api.example.com/errors/deletion-blocked',
                title: 'Deletion currently blocked',
                detail: 'Please resolve the following issues before deletion',
                blockers: blockers.map(b => ({
                    reason: b.reason,
                    resolution: b.resolution,
                })),
            });
        }

        // Löschung einleiten
        const deletion = await this.deletionService.initiate(userId, {
            reason: request.reason,
            feedback: request.feedback,  // Optional
        });

        return {
            status: 'scheduled',
            scheduledAt: deletion.scheduledAt,
            gracePeriodEnds: deletion.gracePeriodEnds,
            retentionNotice,
            canCancel: true,
            cancelUrl: `/users/${userId}/deletion-request/${deletion.id}/cancel`,
        };
    }

    @Delete(':id/deletion-request/:requestId')
    @UseGuards(AuthGuard, SameUserGuard)
    async cancelDeletion(
        @Param('id') userId: string,
        @Param('requestId') requestId: string
    ): Promise<{ status: string }> {
        await this.deletionService.cancel(requestId);
        return {status: 'cancelled'};
    }
}

// Lösch-Service mit Downstream-Propagation
class DeletionService {
    async checkBlockers(
        userId: string
    ): Promise<{blockers: DeletionBlocker[]; retentionNotice?: string}> {
        const blockers: DeletionBlocker[] = [];
        let retentionNotice: string | undefined;

        // Offene Bestellungen
        const openOrders = await this.orderRepository.findOpen(userId);
        if (openOrders.length > 0) {
            blockers.push({
                reason: 'open_orders',
                resolution: 'Please wait for orders to complete or cancel them',
                details: openOrders.map(o => o.id),
            });
        }

        // Aktives Abo
        const subscription = await this.subscriptionRepository.findActive(userId);
        if (subscription) {
            blockers.push({
                reason: 'active_subscription',
                resolution: 'Please cancel your subscription first',
            });
        }

        // Gesetzliche Aufbewahrungspflicht
        const retentionRequired = await this.checkLegalRetention(userId);
        if (retentionRequired) {
            retentionNotice =
                'Einige Daten unterliegen gesetzlichen Aufbewahrungspflichten ' +
                'und werden anonymisiert, aber nicht gelöscht.';
        }

        return {blockers, retentionNotice};
    }

    async executeDelete(userId: string): Promise<void> {
        // 1. Downstream-Services benachrichtigen
        await this.eventBus.publish('user.deletion.started', {userId});

        // 2. Daten anonymisieren (wo Retention gilt)
        await this.anonymizeRetainedData(userId);

        // 3. Daten löschen (wo keine Retention)
        await this.deleteUserData(userId);

        // 4. Bestätigung
        await this.eventBus.publish('user.deletion.completed', {userId});
    }

    private async anonymizeRetainedData(userId: string): Promise<void> {
        // Transaktionen: PII entfernen, aber Summen behalten
        await this.db.query(`
      UPDATE orders
      SET
        customer_name = 'ANONYMIZED',
        customer_email = 'deleted@anonymized.local',
        shipping_address = NULL,
        anonymized_at = NOW()
      WHERE user_id = $1
    `, [userId]);

        // Audit Logs: User-Referenz entfernen
        await this.db.query(`
      UPDATE audit_logs
      SET
        user_id = NULL,
        user_email = 'ANONYMIZED',
        ip_address = NULL
      WHERE user_id = $1
    `, [userId]);
    }
}

Consent Management

// Consent-Modell
interface UserConsent {
    userId: string;
    purpose: ConsentPurpose;
    granted: boolean;
    grantedAt: Date | null;
    revokedAt: Date | null;
    source: 'registration' | 'settings' | 'api';
    version: string;  // Version der Datenschutzerklärung
}

type ConsentPurpose =
    | 'essential'        // Immer erlaubt (Vertrag)
    | 'analytics'        // Nutzungsanalyse
    | 'marketing_email'  // Marketing per Email
    | 'marketing_push'   // Push-Benachrichtigungen
    | 'third_party';     // Weitergabe an Dritte

// API
@Controller('users/:id/consents')
class ConsentController {
    @Get()
    @UseGuards(AuthGuard, SameUserGuard)
    async getConsents(@Param('id') userId: string): Promise<ConsentStatus[]> {
        return this.consentService.getAll(userId);
    }

    @Patch()
    @UseGuards(AuthGuard, SameUserGuard)
    async updateConsents(
        @Param('id') userId: string,
        @Body() updates: ConsentUpdate[]
    ): Promise<ConsentStatus[]> {
        for (const update of updates) {
            if (update.purpose === 'essential') {
                throw new BadRequestException('Essential consent cannot be modified');
            }

            await this.consentService.update(userId, update.purpose, update.granted);

            // Bei Widerruf: Verarbeitung stoppen
            if (!update.granted) {
                await this.stopProcessing(userId, update.purpose);
            }
        }

        return this.consentService.getAll(userId);
    }

    private async stopProcessing(userId: string, purpose: ConsentPurpose): Promise<void> {
        switch (purpose) {
            case 'marketing_email':
                await this.emailService.unsubscribe(userId);
                break;
            case 'analytics':
                await this.analyticsService.excludeUser(userId);
                break;
            case 'third_party':
                await this.partnerService.revokeAccess(userId);
                break;
        }
    }
}

PII-Schutz in Logs und Non-Prod

Personenbezogene Daten dürfen nicht unkontrolliert in Logs oder Test-Umgebungen landen. IP-Adressen sind personenbezogen; besser kürzen oder hashen statt vollständig loggen.

PII in Logs

// Logger mit automatischer PII-Maskierung
class SecureLogger {
    private piiPatterns = [
        {name: 'email', pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g},
        {name: 'phone', pattern: /\+?[0-9]{10,15}/g},
        {name: 'ip', pattern: /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g},
        {name: 'ipv6', pattern: /([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}/g},
        {name: 'credit_card', pattern: /\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}/g},
        {name: 'iban', pattern: /[A-Z]{2}\d{2}[A-Z0-9]{4}\d{7}([A-Z0-9]?){0,16}/g},
    ];

    private sensitiveFields = [
        'password', 'token', 'secret', 'authorization',
        'creditCard', 'ssn', 'dateOfBirth', 'address'
    ];

    log(level: string, message: string, context?: Record<string, any>): void {
        const sanitizedMessage = this.maskPii(message);
        const sanitizedContext = this.sanitizeContext(context);

        this.writeLog({
            level,
            message: sanitizedMessage,
            context: sanitizedContext,
            timestamp: new Date().toISOString(),
        });
    }

    private maskPii(text: string): string {
        let masked = text;
        for (const {pattern} of this.piiPatterns) {
            masked = masked.replace(pattern, '[REDACTED]');
        }
        return masked;
    }

    private sanitizeContext(context?: Record<string, any>): Record<string, any> {
        if (!context) return {};

        const sanitized: Record<string, any> = {};

        for (const [key, value] of Object.entries(context)) {
            if (this.sensitiveFields.some(f => key.toLowerCase().includes(f))) {
                sanitized[key] = '[REDACTED]';
            } else if (typeof value === 'object' && value !== null) {
                sanitized[key] = this.sanitizeContext(value);
            } else {
                sanitized[key] = value;
            }
        }

        return sanitized;
    }
}

// Verwendung
logger.log('info', 'User login', {
    userId: '123',
    email: 'user@example.com',  // → '[REDACTED]'
    ip: '192.168.1.1',          // → '[REDACTED]'
    userAgent: 'Mozilla/5.0',   // → bleibt erhalten
});

PII-Maskierung für Non-Prod

-- Anonymisierungs-Script für Staging/Dev
-- NIEMALS auf Production ausführen!

-- Prüfung: Nicht auf Production ausführen
DO
$$
BEGIN
  IF
current_database() = 'production' THEN
    RAISE EXCEPTION 'Cannot run anonymization on production!';
END IF;
END $$;

-- Users anonymisieren
UPDATE users
SET email         = 'user_' || id || '@test.local',
    first_name    = 'Test',
    last_name     = 'User_' || id,
    phone         = '+49000' || LPAD(id::text, 7, '0'),
    date_of_birth = '1990-01-01'::date + (random() * 10000)::int;

-- Adressen anonymisieren
UPDATE addresses
SET street      = 'Teststraße ' || id,
    city        = 'Teststadt',
    postal_code = '12345',
    country     = 'DE';

-- Zahlungsdaten entfernen
UPDATE payment_methods
SET card_number = NULL,
    card_holder = 'TEST USER',
    expiry      = NULL;

-- Nachrichten/Kommentare
UPDATE messages
SET content = 'Lorem ipsum dolor sit amet...';

-- Audit-Log (sensible Details entfernen)
UPDATE audit_logs
SET ip_address = '127.0.0.1',
    user_agent = 'Anonymized';

Synthetic Data Generation

// Faker-basierte Testdaten statt Produktionsdaten
import {faker} from '@faker-js/faker';

class TestDataGenerator {
    generateUser(): TestUser {
        return {
            id: faker.string.uuid(),
            email: faker.internet.email(),
            first_name: faker.person.firstName(),
            last_name: faker.person.lastName(),
            phone: faker.phone.number(),
            date_of_birth: faker.date.birthdate({min: 18, max: 80, mode: 'age'}),
            address: {
                street: faker.location.streetAddress(),
                city: faker.location.city(),
                postal_code: faker.location.zipCode(),
                country: faker.location.countryCode(),
            },
        };
    }

    generateOrder(userId: string): TestOrder {
        return {
            id: faker.string.uuid(),
            user_id: userId,
            items: Array.from({length: faker.number.int({min: 1, max: 5})}, () => ({
                product_id: faker.string.uuid(),
                name: faker.commerce.productName(),
                quantity: faker.number.int({min: 1, max: 3}),
                price: parseFloat(faker.commerce.price()),
            })),
            total: parseFloat(faker.commerce.price({min: 10, max: 500})),
            created_at: faker.date.recent({days: 30}),
        };
    }
}

Data Residency

Für Multi-Tenant-Systeme oder internationale Dienste: Wo werden Daten gespeichert? Die DSGVO verlangt keine generelle EU-Speicherung, aber Drittlandtransfers brauchen eine Rechtsgrundlage (z. B. Angemessenheitsbeschluss oder geeignete Garantien wie SCC/BCR).

Residency-Anforderungen

EU/EWR

  • Verarbeitung in EU/EWR vereinfacht Compliance
  • Drittlandtransfer nur mit Rechtsgrundlage (Art. 45 Angemessenheit oder Art. 46 Garantien)

Branchenspezifisch

  • Zusätzliche Vorgaben je nach Branche/Vertrag prüfen

Schweiz/UK

  • EU-Angemessenheitsbeschluss (Liste prüfen)

USA

  • Angemessenheit nur für EU-US DPF-zertifizierte Firmen
  • Alternativ: SCC/BCR + TIA

Multi-Region Architektur

// Tenant-basiertes Routing
class DataResidencyRouter {
    private regionConfigs = new Map<string, RegionConfig>([
        ['eu', {
            database: 'postgres-eu.example.com',
            storage: 's3-eu-central-1',
            allowedCountries: ['DE', 'FR', 'IT', 'ES', 'NL', /* ... */],
        }],
        ['us', {
            database: 'postgres-us.example.com',
            storage: 's3-us-east-1',
            allowedCountries: ['US', 'CA', 'MX'],
        }],
        ['ch', {
            database: 'postgres-ch.example.com',
            storage: 'azure-switzerland-north',
            allowedCountries: ['CH', 'LI'],
        }],
    ]);

    getConnectionForTenant(tenantId: string): DatabaseConnection {
        const tenant = this.tenantRepository.findById(tenantId);
        const region = this.regionConfigs.get(tenant.dataRegion);

        if (!region) {
            throw new Error(`Unknown region: ${tenant.dataRegion}`);
        }

        return this.connectionPool.get(region.database);
    }

    validateDataTransfer(
        fromRegion: string,
        toRegion: string,
        basis: 'adequacy' | 'scc' | 'bcr'
    ): boolean {
        if (fromRegion === toRegion) {
            return true;
        }

        if (basis === 'adequacy') {
            if (fromRegion === 'eu' && toRegion === 'us') {
                return this.isDpfCertified(toRegion);
            }
            return this.isAdequateDestination(fromRegion, toRegion);
        }

        // SCC/BCR: zusätzliche Garantien + TIA erforderlich
        return this.hasSafeguards(fromRegion, toRegion);
    }

    private isAdequateDestination(fromRegion: string, toRegion: string): boolean {
        const adequate = new Map([
            ['eu', ['ch', 'uk']],
            ['ch', ['eu']],
            ['uk', ['eu']],
        ]);

        return adequate.get(fromRegion)?.includes(toRegion) ?? false;
    }

    private isDpfCertified(toRegion: string): boolean {
        // Beispiel: Registry/API-Abfrage; hier vereinfacht via Allowlist
        const certifiedStorageByRegion = new Map<string, string[]>([
            ['us', ['s3-us-east-1']],
        ]);

        const recipientStorage = this.regionConfigs.get(toRegion)?.storage;
        return recipientStorage
            ? certifiedStorageByRegion.get(toRegion)?.includes(recipientStorage) ?? false
            : false;
    }

    private hasSafeguards(fromRegion: string, toRegion: string): boolean {
        // Beispiel: SCC/BCR + Transfer Impact Assessment als Policy-Matrix
        const safeguards = new Map<string, {scc: boolean; tia: boolean}>([
            ['eu->us', {scc: true, tia: true}],
            ['eu->ch', {scc: true, tia: true}],
            ['ch->us', {scc: true, tia: false}],
        ]);

        const key = `${fromRegion}->${toRegion}`;
        const measures = safeguards.get(key);
        return Boolean(measures?.scc && measures?.tia);
    }
}

Regeln und Anti-Patterns

Do

  • Data Minimization: Nur erheben, was wirklich benötigt wird
  • Retention Policies: Automatische Löschung nach definierten Fristen
  • Soft Delete mit Grace Period: Versehentliche Löschungen rückgängig machen
  • PII-Maskierung: In Logs und Non-Prod keine echten Daten
  • Consent-Management: Einwilligungen sauber verwalten und respektieren
  • Export-Format: Maschinenlesbar (JSON, CSV), nicht PDF

Don't

  • PII in URLs: Keine E-Mail-Adressen oder Namen in Query-Parametern
  • Logs ohne Maskierung: PII in Logs ist ein Audit-Failure
  • Prod-Daten in Dev: Immer synthetische oder anonymisierte Daten
  • Löschung ignorieren: DSGVO-Anfragen haben Fristen (1 Monat, ggf. verlängert)
  • Consent vorauswählen: Opt-in, nicht Opt-out
  • Unbegrenzte Retention: Jedes Datum braucht ein Verfallsdatum

Artefakt: Data-Lifecycle-Policy

# Data Lifecycle Policy

## 1. Datenklassifizierung

| Klasse | Beschreibung | Beispiele | Schutzlevel |
|--------|--------------|-----------|-------------|
| PII | Personenbezogene Daten | Name, Email, Adresse | Hoch |
| Sensitive PII | Besonders schützenswert | Gesundheit, Finanzen | Sehr hoch |
| Business | Geschäftsdaten | Bestellungen, Verträge | Mittel |
| Technical | Technische Daten | Logs, Metriken | Niedrig |
| Public | Öffentliche Daten | Produktinfos | Keine Beschränkung |

## 2. Retention Schedule

| Datentyp | Retention | Rechtliche Basis | Löschmethode |
|----------|-----------|------------------|--------------|
| Account (aktiv) | Bis Zweckende + Inaktivitätsfrist | Vertrag | - |
| Account (gelöscht, Self-Service) | 14–30 Tage | Wiederherstellung | Hard Delete |
| Buchungsbelege/Rechnungen (DE) | 8–10 Jahre | HGB §257, AO §147 | Archiv → Delete |
| Handels-/Geschäftsbriefe (DE) | 6 Jahre | HGB §257, AO §147 | Archiv → Delete |
| Audit Logs | 6–12 Monate | Compliance | Rotation |
| Access Logs | 7–30 Tage | Debugging/Security | Rotation |
| Marketing-Daten | Bis Widerruf | Einwilligung | Hard Delete |
| Analytics (PII) | 0 | - | Aggregieren/Anonymisieren |
| Session Data | 24h-30d | Funktion | Auto-Expire |

## 3. DSGVO-Prozesse

### Auskunftsanfrage (Art. 15)

- **Frist**: 1 Monat (+ bis zu 2 Monate möglich, informieren)
- **Format**: JSON, optional PDF-Zusammenfassung
- **Prozess**: Automatisierter Export via API
- **Verantwortlich**: Automatisch / Datenschutzbeauftragter bei Sonderfällen

### Löschanfrage (Art. 17)

- **Frist**: 1 Monat (+ bis zu 2 Monate möglich, informieren)
- **Prüfung**: Automatische Blocker-Erkennung
- **Grace Period**: 14–30 Tage (nur Self-Service, nicht bei Art. 17)
- **Ausnahmen**: Gesetzliche Aufbewahrungspflichten (anonymisieren statt löschen)

### Berichtigung (Art. 16)

- **Frist**: 1 Monat (+ bis zu 2 Monate möglich, informieren)
- **Prozess**: Self-Service via UI/API
- **Audit**: Änderungen protokollieren

### Datenübertragbarkeit (Art. 20)

- **Format**: JSON, CSV
- **Scope**: Vom Nutzer bereitgestellte/observierte Daten; Einwilligung/Vertrag
- **Prozess**: Async Export mit Download-Link

## 4. PII-Handling

### In Logs

- Automatische Maskierung aktiv
- Maskierte Felder: E-Mail, IP, Telefon, Kreditkarte
- Review: Quartalsweise Log-Audit

### In Non-Prod

- Keine Produktionsdaten ohne Anonymisierung
- Anonymisierungs-Script: `scripts/anonymize-db.sql`
- Synthetische Daten: Faker.js

### In Backups

- Verschlüsselung: AES-256
- Retention: 30 Tage
- Geo-Redundanz: Nur innerhalb der Datenregion
- Löschungen: Wirksam nach Ablauf der Backup-Retention; Restore → Re-Delete

## 5. Data Residency

| Region | Speicherort | Erlaubte Transfers |
|--------|-------------|-------------------|
| EU | eu-central-1 | EU, CH, UK (Angemessenheit) |
| CH | switzerland-north | CH, EU (Angemessenheit) |
| US | us-east-1 | US (DPF) oder SCC/BCR |

## 6. Verantwortlichkeiten

| Rolle | Verantwortung |
|-------|---------------|
| Datenschutzbeauftragter | Policy-Owner, DSGVO-Anfragen |
| Engineering | Technische Umsetzung |
| Operations | Backup, Retention, Löschung |
| Legal | Rechtliche Bewertung, Verträge |

Checkliste

Must-have vor Go-Live

  • [ ] Data Minimization: Nur notwendige Daten erheben
  • [ ] Retention Policies für alle Datentypen definiert
  • [ ] Automatische Löschung implementiert
  • [ ] Datenexport (Art. 15/20) möglich
  • [ ] Löschprozess (Art. 17) implementiert
  • [ ] PII-Maskierung in Logs aktiv
  • [ ] Non-Prod ohne echte PII

Should-have

  • [ ] Consent-Management mit Widerrufsmöglichkeit
  • [ ] Soft Delete mit Grace Period
  • [ ] Anonymisierungs-Scripts für Non-Prod
  • [ ] Data Residency dokumentiert

Nice-to-have

  • [ ] Self-Service Datenexport
  • [ ] Automatische Consent-Versionierung
  • [ ] Data Lineage Tracking

Wie es weitergeht

Im letzten Teil der Serie behandeln wir Go-Live-Readiness – die finale Checkliste, die alle Themen zusammenführt und sicherstellt, dass deine API bereit für den Launch ist.


Dies ist Teil 21 der Serie API-Design. Alle Teile findest du in der Serie: API-Design.

Mehr Beiträge aus dem Blog.