Eine E-Mail = eine Identität
Kurz: Zwei Mitglieder oder Mitarbeiter mit derselben E-Mail-Adresse sind in Prilog datenbankseitig unmöglich. Eltern und Geschwister dürfen sich weiterhin eine Familien-Adresse teilen. Diese Seite erklärt, warum das so gebaut ist und wie man Alt-Dubletten findet und zusammenführt.
Das Problem, das dahinter steckt
Eine Person lebt in Prilog in drei verbundenen Tabellen:
| Tabelle | Rolle |
|---|---|
| Person | Die eine Wahrheit zu einem Menschen (Stammdaten) |
| UserAccount | Sein Login (Benutzername, Matrix-Konto, Login-E-Mail) |
| UserDirectoryEntry | Der Verzeichnis-Eintrag (Projektion fürs Chat-System) |
Historisch trug jede dieser Tabellen eine E-Mail-Adresse, aber nichts verglich sie. Nur das Login-Konto hatte eine (verdeckte) Eindeutigkeits- Regel. Ein Massen-Import prüfte beim Anlegen nur den technischen Schlüssel (Benutzername / Matrix-ID) — nie die E-Mail. Dadurch konnte derselbe Mensch, der bereits als „broder" existierte, ein zweites Mal als „bbuch" angelegt werden: gleiche E-Mail, zwei Personen, zwei Mitgliedschaften. Genau das ist im Juli 2026 passiert („Broder Buch" doppelt).
Die Regel
Prilog kennt jetzt einen E-Mail-Eindeutigkeits-Schlüssel (emailIdentityKey). Er entscheidet, ob eine E-Mail nur einmal vorkommen darf:
- Mitarbeiter & alle mit Login → die E-Mail ist eindeutig. Ein zweiter Datensatz mit derselben Adresse wird von der Datenbank abgewiesen.
- Eltern, Schüler und externe Kontakte ohne eigenen Login → dürfen sich eine Familien-Adresse teilen. Für sie gilt keine Sperre.
Das entspricht der Schul-Realität: Geschwister und Eltern nutzen oft eine Adresse, während jede Lehrkraft ihre eigene braucht.
Warum diese Grenze?
Ein Login ist eine E-Mail — zwei Menschen können sich nicht mit derselben Adresse anmelden. Genau da muss die Eindeutigkeit hart sein. Kontakte ohne Login sind reine Stammdaten; dort ist eine geteilte Adresse legitim.
Dubletten finden und zusammenführen
Unter Einstellungen → Benutzer (bzw. der Admin-Konsole) gibt es den Dubletten-Report. Er listet:
- Personen/Verzeichnis-Einträge mit gleicher E-Mail (sollte dank der Sperre nicht neu entstehen — zeigt nur Alt-Bestand),
- Personen mit exakt gleichem Namen (schwächeres Signal, nur Hinweis).
Zu jeder Gruppe schlägt der Report vor, welchen Datensatz man behält (der mit Login, sonst der älteste). Das Zusammenführen (Merge) biegt alle Verknüpfungen — Mitgliedschaften, Beziehungen, Akten, Aktivitäten — vom Duplikat auf den behaltenen Datensatz um und löscht das Duplikat.
Zusammenführen ist endgültig
Der Merge kann nur vom obersten Administrator ausgelöst werden und ist nicht umkehrbar. Haben beide Datensätze ein Login oder eine Schülerakte, verweigert Prilog den automatischen Merge — dann muss zuerst ein Konto deaktiviert/archiviert werden.
Was passiert beim Import jetzt?
Der Massen-Import (und jeder Einzel-Anlage-Pfad) prüft vor dem Anlegen, ob bereits ein Mitglied mit dieser E-Mail existiert:
- Treffer → die Zeile wird abgewiesen mit klarer Meldung („E-Mail … bereits an ‚Broder Buch' vergeben"), statt eine Dublette zu erzeugen.
- Gleiche E-Mail doppelt in einer Datei → die zweite Zeile wird als Konflikt markiert, bevor irgendetwas geschrieben wird.
Für Entwickler: wie es gebaut ist
Das Konzept ist verteidigt in Schichten — fällt eine, hält die nächste.
Schicht 1 — DB-Zwang (die Garantie). Migration 0206_email_identity_uniqueness fügt email_identity_key auf persons und user_directory_entries hinzu und legt partielle Unique-Indizes pro Tenant an: persons_tenant_email_identity_unique, user_directory_entries_tenant_email_identity_unique (jeweils WHERE email_identity_key IS NOT NULL AND … NOT archived/deleted). Der bereits bestehende user_accounts_tenant_login_email_unique (aus 0078, in schema.prisma unsichtbar) ist jetzt im Schema kommentiert.
Schicht 2 — eine Policy, eine Auflösung.src/lib/identity-email.ts ist die einzige Stelle für Normalisierung (normalizeEmail) und die Regel (computeEmailIdentityKey → Key nur bei Login oder audience='staff', sonst null). src/services/identity-resolver.service.ts bietet findMemberPersonByEmail (Vorabprüfung) und findDuplicateIdentities (Report).
Schicht 3 — die Schreibpfade. Alle setzen jetzt den Key und prüfen vorab per E-Mail: person-provisioning.service (kanonisch), workspace.router Bulk-Import (Lager und aktiv), person-from-contact.service, import-staging.service (inkl. Dubletten- Schutz innerhalb einer Datei), bulk-create-logins.service, customer/users.router (POST /users). P2002-Kollisionen werden graziös als Konflikt zurückgegeben statt als Server-Fehler.
Schicht 4 — Report & Sichtbarkeit.GET /workspace/identity-duplicates (Admin) liefert die Cluster.
Schicht 5 — Merge. src/services/person-merge.service.ts (mergePersons) biegt alle Person-Fremdschlüssel um und löscht das Duplikat; POST /workspace/identity-merge (nur SUPERADMIN) ist die Route. scripts/merge-identity-shadows.ts bereinigt Alt-Bestand vor der Migration (sonst kann der Unique-Index nicht gebaut werden).
Reihenfolge beim Ausrollen
- Merge-/Resolver-/Policy-Code deployen.
merge-identity-shadows.ts --apply(Alt-Dubletten zusammenführen).prisma migrate deploy(baut jetzt die Unique-Indizes über sauberen Daten).prisma generate+ Backend-Neustart.