Technische und organisatorische Maßnahmen
Anlage 1 zum Auftragsverarbeitungsvertrag (Art. 32 DSGVO) — Stand 05.09.2026
Diese Seite beschreibt, wie Nuhria die Daten der Praxen und ihrer Klientinnen und Klienten schützt. Sie ist Anlage 1 des Auftragsverarbeitungsvertrags und ergänzt die Datenschutzerklärung. Aufgeführt ist nur, was der Programmcode oder die Betriebsdokumentation belegt; zu jeder Maßnahme steht die Fundstelle. Punkte, die die Plattform-Anbieter (Vercel, Neon, Resend) zusagen und die Nuhria nicht selbst umsetzt, sind als Anbieterangabe gekennzeichnet.
Zutrittskontrolle
Wer kommt körperlich an die Rechner heran, auf denen Nuhria und die Datenbank laufen?
Nuhria betreibt keine eigenen Server und keinen eigenen Serverraum. Die Anwendung läuft beim Anbieter Vercel in der Region Frankfurt am Main (Kennung fra1), die Datenbank beim Anbieter Neon in der AWS-Region eu-central-1 (Frankfurt am Main). Hochgeladene Dateien liegen in derselben Datenbank, nicht auf einem Dateiserver.
Beleg: vercel.json (regions), lib/prisma.ts, Datenbank-Adresse in den Umgebungsvariablen (eu-central-1), prisma/schema.prisma (Modell FileBlob)
Den Zutritt zu den Rechenzentren regeln die Anbieter (Zutrittskontrolle, Videoüberwachung, Zertifizierungen der Rechenzentrumsbetreiber). Nuhria hat keinen physischen Zugang.
AnbieterangabeBeleg: Angaben von Vercel und Neon zu ihren Rechenzentren
Der Zugang zu den Verwaltungsoberflächen von Vercel und Neon ist auf den Anbieter von Nuhria (eine Person, siehe Impressum) beschränkt.
Beleg: Organisatorische Regelung; Impressum
Zugangskontrolle
Wie wird verhindert, dass Unbefugte sich bei Nuhria anmelden?
Passwörter werden niemals im Klartext gespeichert, sondern mit dem Verfahren bcrypt (Kostenfaktor 10) gehasht. Die Mindestlänge beträgt 12 Zeichen — für Coaches und für Klientinnen und Klienten gleichermaßen. Konten, die über Google angelegt wurden, erhalten ein zufälliges Passwort.
Beleg: lib/auth.ts (hashPassword/verifyPassword), lib/password-policy.ts (PASSWORD_MIN_LENGTH)
Eine Anmeldung erzeugt eine Sitzung mit einem 32 Byte langen Zufallswert, der nur in der Datenbank und im Browser-Cookie liegt. Die Sitzung endet nach 30 Tagen. Das Cookie ist für Skripte im Browser nicht lesbar (httpOnly), wird nicht an fremde Seiten mitgeschickt (SameSite=Lax) und im Live-Betrieb nur verschlüsselt übertragen (Secure).
Beleg: lib/auth.ts (newToken, persistSession, sessionCookieOptions)
Abgelaufene Sitzungen werden beim nächsten Aufruf aus der Datenbank entfernt. Coaches sehen unter Einstellungen → Konto ihre angemeldeten Geräte und können einzelne oder alle anderen Sitzungen beenden. Eine Passwortänderung beendet alle anderen Sitzungen; ein Passwort-Reset beendet alle Sitzungen.
Beleg: lib/auth.ts (getCurrentSession), app/actions/account.ts (revokeSessionAction, revokeOtherSessionsAction, Passwort ändern), app/actions/auth.ts (Reset)
Schutz gegen Passwort-Raten in drei Stufen: höchstens 30 Fehlversuche je IP-Adresse in 15 Minuten; ab dem vierten Fehlversuch je Konto eine wachsende Wartezeit (2, 4, 8 Sekunden); nach 10 Fehlversuchen eine Sperre des Kontos für 30 Minuten mit einer E-Mail, deren Link die Sperre aufhebt (24 Stunden gültig). Die Antwort der Anmeldemaske verrät nicht, ob eine E-Mail-Adresse ein Konto hat. Sperre und Entsperrung werden im Protokoll der Praxis vermerkt.
Beleg: lib/login-guard.ts (LOGIN_IP_LIMIT, LOGIN_LOCK_AFTER, LOGIN_LOCK_MS, UNLOCK_TOKEN_MS), lib/audit.ts (login.locked, login.unlocked)
Zweiter Faktor für Coach-Konten: Coaches können unter Einstellungen → Konto eine Authentifizierungs-App (zeitbasierte Einmalcodes, TOTP) einrichten. Das Geheimnis dafür wird mit AES-256-GCM verschlüsselt gespeichert; der Schlüssel wird aus dem Server-Geheimnis SESSION_SECRET abgeleitet und liegt nicht in der Datenbank. Die zehn Backup-Codes werden nur als bcrypt-Hashes gespeichert und gelten je einmal. Einrichten und Abschalten erfordern das Passwort; Codeprüfungen sind auf 10 Versuche je 15 Minuten begrenzt.
Beleg: lib/totp.ts (encryptSecret/decryptSecret, verifyTotp), lib/totp-backup.ts, app/actions/account.ts (beginTotpSetupAction, confirmTotpSetupAction, disableTotpAction)
Anmeldung mit Google-Konto nur über das OAuth-Verfahren mit einem zufälligen Prüfwert (State) in einem httpOnly-Cookie, das nach 10 Minuten verfällt. Nuhria sieht nie ein Google-Passwort.
Beleg: lib/google-oauth.ts (oauthCookieOptions), app/api/auth/google/*
Alle empfindlichen Schritte sind in der Datenbank ratenbegrenzt (Registrierung, Passwort zurücksetzen, Passwort festlegen, E-Mail-Adresse ändern, Bestätigungslinks, Google-Anmeldung, öffentliche Buchung, Videoraum-Signalisierung, Zugangsdaten für den Vermittlungsserver, Lebensmittelsuche).
Beleg: lib/rate-limit.ts (rateLimit) und die Aufrufe in app/actions/*, app/api/*
Technische Schnittstellen sind abgesichert: Der tägliche Automations-Lauf akzeptiert nur ein geheimes Bearer-Token; Zahlungsmeldungen von Stripe werden nur mit gültiger Signatur angenommen; interne Hilfsseiten (/dev) sind im Live-Betrieb unerreichbar.
Beleg: app/api/cron/tick/route.ts (CRON_SECRET), app/api/stripe/webhook/route.ts (constructEventAsync), middleware.ts (/dev)
Zugriffskontrolle
Wer darf nach der Anmeldung welche Daten sehen und ändern?
Jede Praxis ist ein eigener Mandant. Eine Praxis gehört genau einem Coach-Konto; jede Seite und jede Aktion im Arbeitsbereich prüft zuerst die Rolle und die Praxis der angemeldeten Person (requireCoach). Klientinnen und Klienten sehen im Portal nur ihre eigene Akte (requireClient/requireActiveClient).
Beleg: lib/auth.ts (requireCoach, requireClient, requireActiveClient, practiceOf), prisma/schema.prisma (Practice.userId 1:1)
Dateien werden nur mit gültiger Sitzung ausgeliefert: Coaches erhalten nur Dateien ihrer Praxis, Klientinnen und Klienten nur ihre eigenen. Ausgeliefert wird nur eine feste Liste bekannter Dateitypen mit ihrem echten Typ (Bilder, PDF, einfache Textdateien); im Browser angezeigt werden davon allein Bilder und PDF, alles andere kommt als Download. Unbekannte Typen erhalten stets einen neutralen Typ und werden heruntergeladen — so kann kein eingeschleustes Skript im Browser laufen. Dateiantworten sind vom Zwischenspeichern ausgenommen.
Beleg: app/files/[...path]/route.ts (SAFE_MIME, Cache-Control: private, no-store)
Der Videoraum eines Termins öffnet nur für den Coach der Praxis und die betreute Person des Termins, nur bei Status „geplant“ und nur 30 Minuten vor Beginn bis 60 Minuten nach Ende. Ein nicht vorhandener und ein fremder Termin erhalten dieselbe Antwort. Zugangsdaten für den Vermittlungsserver (TURN) entstehen erst nach dieser Prüfung, gelten zeitlich begrenzt (Vorgabe 1 Stunde) und werden nie in Seiten eingebettet.
Beleg: lib/call.ts (authorizeCall), app/api/call/[id]/route.ts, app/api/call/[id]/ice/route.ts, lib/turn.ts
Sieht sich ein Coach die Portal-Ansicht einer Klientin oder eines Klienten an, läuft das über eine gekennzeichnete Vorschau-Sitzung, in der jede schreibende Aktion abgelehnt wird; sie endet spätestens nach 24 Stunden.
Beleg: lib/auth.ts (createPortalPreviewSession), lib/portal-user.ts (portalVorschauFehler)
Geheimnisse (Datenbank-Adresse, Sitzungsschlüssel, API-Schlüssel) liegen ausschließlich in Umgebungsvariablen des Hostings, nicht im Quellcode; die lokale .env ist von der Versionsverwaltung ausgeschlossen.
Beleg: .gitignore (.env*), .env.example
Trennungskontrolle
Wie bleiben die Daten verschiedener Praxen und verschiedener Zwecke voneinander getrennt?
Alle Praxisdaten (Klienten, Termine, Nachrichten, Formulare, Dateien, Rechnungen, Protokoll) tragen die Kennung ihrer Praxis; Abfragen laufen immer über die Praxis der angemeldeten Person. Ein Zugriff über Praxisgrenzen hinweg ist im Datenmodell nicht vorgesehen.
Beleg: prisma/schema.prisma (practiceId in den Praxis-Tabellen), lib/auth.ts
Entwicklung und Live-Betrieb sind getrennt: Test- und Beispieldaten-Skripte brechen ab, wenn die Datenbank-Adresse nicht auf einen lokalen Rechner zeigt.
Beleg: prisma/seed.ts (Prüfung auf localhost), .env.example (Warnhinweise)
Der Auftragsverarbeiter verarbeitet die Klientendaten einer Praxis ausschließlich für diese Praxis — keine Auswertung über Praxen hinweg, keine Weitergabe zu Werbezwecken.
Beleg: Datenschutzerklärung Abschnitt 6; AVV Abschnitt 3
Pseudonymisierung und Verschlüsselung
Welche Daten sind verschlüsselt oder unkenntlich gemacht?
Passwörter und Backup-Codes des zweiten Faktors: nur als bcrypt-Hashes. Geheimnis der Authentifizierungs-App: AES-256-GCM. Sitzungs-, Einladungs-, Reset- und Entsperr-Links: 32 Byte Zufallswerte.
Beleg: lib/auth.ts, lib/totp.ts, lib/totp-backup.ts, lib/login-guard.ts (newUnlockToken)
Verschlüsselung der gespeicherten Daten (Datenträger, Sicherungen) übernimmt der Datenbank-Anbieter Neon.
AnbieterangabeBeleg: Angaben von Neon zur Verschlüsselung ruhender Daten
Fehlermeldungen und Alarme an den Anbieter enthalten Pfad, Fehlercode und Zeit, aber keine Inhalte aus Akten, Nachrichten oder Formularen.
Beleg: instrumentation.ts (onRequestError), lib/reportError.ts
Weitergabekontrolle
Wie sind Daten auf dem Weg zwischen Browser, Nuhria, Datenbank und Dienstleistern geschützt?
Alle Aufrufe von nuhria.de laufen über TLS (https). Zertifikate und die Umleitung von http auf https stellt die Hosting-Plattform Vercel bereit.
AnbieterangabeBeleg: Angaben von Vercel (TLS/HSTS auf Plattformebene)
Die Verbindung zwischen Anwendung und Datenbank ist verschlüsselt (Datenbank-Adresse mit sslmode=require).
Beleg: DATABASE_URL in den Umgebungsvariablen (sslmode=require), lib/prisma.ts
Sicherheits-Kopfzeilen auf allen Antworten: X-Content-Type-Options nosniff, X-Frame-Options DENY (kein Einbetten in fremde Seiten), Referrer-Policy strict-origin-when-cross-origin, Permissions-Policy (Kamera, Mikrofon und Bildschirmfreigabe nur für nuhria.de selbst, keine Standortabfrage). Seiten unter /app und /portal werden nicht zwischengespeichert (Cache-Control no-store).
Beleg: next.config.ts (headers), middleware.ts
Videotermine: Bild und Ton laufen Ende-zu-Ende verschlüsselt (DTLS-SRTP, Bestandteil von WebRTC) und nach Möglichkeit direkt zwischen den Geräten. Über Nuhria läuft nur die Signalisierung; ein Vermittlungsserver (TURN) sieht nur verschlüsselte Pakete.
Beleg: components/video-call.tsx, lib/call.ts, lib/turn.ts, docs/TURN.md
Die Verbindung mit einem Google Kalender ist freiwillig und im Auslieferungszustand ausgeschaltet. Richtet eine Praxis sie ein, überträgt Nuhria ausschließlich die Termindaten dieser Praxis (Titel mit Namen der betreuten Person, Beginn und Ende, Anschrift oder Link zum Videoraum, Terminnotiz) in den Kalender dieser Praxis. Die Google-Zugangsdaten werden beim Praxiskonto in der Datenbank gespeichert, nur serverseitig verwendet und erreichen den Browser nie; beim Trennen der Verbindung werden sie gelöscht.
Beleg: lib/google-calendar.ts (upsertGoogleEvent, syncAppointmentToGoogle), app/actions/account.ts (disconnectGoogleCalendarAction), app/app/einstellungen (Verbindungen)
E-Mails versendet der Dienst Resend (USA) auf Grundlage der Standardvertragsklauseln; eine Kopie jeder E-Mail bleibt 12 Monate in der Datenbank und wird dann automatisch gelöscht.
Beleg: lib/email.ts, lib/automations.ts (MAILBOX_RETENTION_MONTHS)
Datenausgabe an betroffene Personen und Praxen nur über angemeldete, geprüfte Wege: Klientenakte als Datei (Coach), eigene Daten (Portal), gesamte Praxis (Coach).
Beleg: lib/gdpr-export.ts, app/api/export/client/[id]/route.ts, app/api/export/me/route.ts, app/api/export/practice/route.ts
Eingabekontrolle
Lässt sich nachvollziehen, wer wann welche Daten eingegeben oder verändert hat?
Praxisgebundenes Protokoll mit Zeitpunkt, handelnder Person und Betroffenem für diese Ereignisse: Klient angelegt, Rechnung bezahlt, Formular gesendet, Anmeldung nach zu vielen Fehlversuchen gesperrt, Anmeldesperre aufgehoben. Weitere Ereignisarten werden nach Bedarf ergänzt.
Beleg: lib/audit.ts (writeAudit, AUDIT_LABELS), prisma/schema.prisma (AuditLog)
Notizen, Nachrichten, Formularantworten, Messwerte und Tagebucheinträge tragen Zeitstempel und, wo vorgesehen, die verfassende Person.
Beleg: prisma/schema.prisma (createdAt/updatedAt, Note.author)
Sitzungen speichern Gerät und Browser (User-Agent) und den Zeitpunkt, damit Coaches fremde Anmeldungen erkennen können.
Beleg: lib/auth.ts (persistSession), app/app/einstellungen (Geräteliste)
Verfügbarkeitskontrolle und Wiederherstellung
Was passiert bei Ausfall, Fehler oder Datenverlust?
Sicherungskopie der gesamten Datenbank einschließlich hochgeladener Dateien per Skript; ein abgebrochener Lauf wird sichtbar als unvollständig gekennzeichnet. Die Rücksicherung ist dokumentiert.
Beleg: scripts/backup-db.mjs (BACKUP-UNVOLLSTAENDIG.txt), ROLLBACK.md
Der Datenbank-Anbieter Neon hält zusätzlich eine zeitpunktgenaue Wiederherstellung (Point-in-Time-Restore) vor.
AnbieterangabeBeleg: Angaben von Neon; ROLLBACK.md
Jede Auslieferung der Anwendung bei Vercel ist unveränderlich; eine frühere Version lässt sich jederzeit wieder aktivieren. Der Quellcode liegt versioniert vor.
AnbieterangabeBeleg: Vercel-Deployments; lokales Git-Repository (ROLLBACK.md)
Unbehandelte Serverfehler lösen sofort einen Alarm an den Anbieter aus (E-Mail, optional Webhook), ohne personenbezogene Inhalte.
Beleg: instrumentation.ts, lib/reportError.ts
Schutz vor Überlast: begrenzter Datenbank-Verbindungspool je Instanz, Ratenbegrenzung auf empfindlichen Wegen, tägliche Aufräum-Läufe (Signalisierungsdaten des Videoraums älter als 24 Stunden, E-Mail-Kopien älter als 12 Monate).
Beleg: lib/prisma.ts (PRISMA_POOL), lib/rate-limit.ts, lib/automations.ts (runTick)
Löschung und Speicherbegrenzung
Wie und wann werden Daten wieder entfernt?
Coaches können Klientenakten löschen; das Löschen der Praxis oder des eigenen Kontos entfernt alle zugehörigen Daten in einem Schritt (Transaktion) — Rechnungsdaten nur, soweit keine gesetzliche Aufbewahrungspflicht entgegensteht.
Beleg: app/actions/clients.ts (deleteClientAction), app/actions/account.ts (deletePracticeAction, deleteAccountAction)
Feste Fristen: E-Mail-Kopien 12 Monate; Signalisierung des Videoraums 24 Stunden; Sitzungen 30 Tage; Prüfwerte der Google-Anmeldung 10 Minuten; Entsperr-Links 24 Stunden; Zugangsdaten für den Vermittlungsserver 1 Stunde.
Beleg: lib/automations.ts, lib/auth.ts, lib/google-oauth.ts, lib/login-guard.ts, lib/turn.ts
Nach Vertragsende werden die Daten der Praxis innerhalb von 30 Tagen gelöscht, sofern die Praxis sie nicht vorher als Datei ausgibt (AVV Abschnitt 13).
Beleg: AVV Abschnitt 13; app/api/export/practice/route.ts
Auftragskontrolle und Überprüfung
Wie wird sichergestellt, dass nur nach Weisung der Praxis verarbeitet wird — und dass die Maßnahmen weiter wirken?
Vertrag zur Auftragsverarbeitung nach Art. 28 DSGVO mit jeder Praxis, abgeschlossen per Klick bei der Registrierung und mit Zeitpunkt und Fassung gespeichert; Unterauftragsverarbeiter sind namentlich mit Sitz und Zweck aufgeführt (Anlage 2 des AVV).
Beleg: lib/avv-text.ts, lib/avv-version.ts, prisma/schema.prisma (Practice.avvAcceptedAt, avvVersion)
Mit allen Unterauftragsverarbeitern bestehen Verträge zur Auftragsverarbeitung; bei Anbietern mit Sitz in den USA auf Grundlage der Standardvertragsklauseln (SCC).
Beleg: AVV Anlage 2; Datenschutzerklärung Abschnitt 6
Prüfskripte gegen die Live-Umgebung (Kopfzeilen, Anmeldeschutz, Buchung, Videoraum) laufen vor und nach Auslieferungen; die Ergebnisse werden in der Betriebsdokumentation festgehalten.
Beleg: scripts/_live-*-check.mjs, tests/smoke-booking.spec.ts, STATUS.md
Änderungen an dieser Liste werden mit neuem Stand-Datum veröffentlicht; eine inhaltliche Änderung des AVV erhält eine neue Fassung, der die Praxis erneut zustimmt.
Beleg: lib/tom-text.ts (TOM_STAND), lib/avv-version.ts (AVV_VERSION)
In Vorbereitung
Damit diese Liste ehrlich bleibt, stehen hier auch die Maßnahmen, die vorbereitet, aber noch nicht umgesetzt sind:
- Content-Security-Policy-Kopfzeile (zunächst im Beobachtungsmodus, dann verbindlich).
- Automatisierte tägliche Sicherungskopie außerhalb des Datenbank-Anbieters (heute: manueller Lauf des Sicherungsskripts plus Point-in-Time-Restore von Neon).
- Weitere Ereignisarten im Protokoll (Datenexport, Löschung, Einwilligungen).
Fragen zu den Maßnahmen beantworten wir per E-Mail an hallo@nuhria.de.