# Mandantentrennung — Zugriffsklassifikation Dieses Dokument gehört zusammen mit `docs/mandantentrennung-datenbankrolle.md` zur Vorbereitung der Mandantentrennung auf Datenbankebene (WINDOWS #18/#20). Während die Datenbankrolle-Anleitung beschreibt, **wie** die Umstellung technisch abläuft, hält dieses Dokument fest, **welche** der 227 `this.prisma.*`-Fundstellen in `apps/api/src` beim Umbau in den folgenden Etappen angefasst werden müssen und welche bewusst unverändert bleiben. Die Bestandsaufnahme unten wird **maschinell aus dem Quelltext ermittelt** (nicht von Hand zusammengeschrieben) und durch `apps/api/src/prisma/rls-access-inventory.spec.ts` bei jedem Testlauf gegen den tatsächlichen Stand geprüft. Verschiebt sich eine Zeile, bleibt die Prüfung grün — Vergleichsschlüssel sind Datei und Prisma-Modellname, keine Zeilennummer. Kommt eine neue Fundstelle hinzu oder verschwindet eine bestehende, schlägt die Prüfung fehl, bis dieses Dokument nachgezogen wird. ## Die drei Klassen - **`muss-mandantengebunden`** — berührt auf Rechnung genau eines Mandanten eine Tabelle mit `tenantId` (oder eine Tabelle, die ihre Mandantenregel über einen Join auf eine solche Tabelle bezieht). Muss in Etappe 2 auf `forTenant()` umgestellt werden. - **`bewusst-uebergreifend`** — muss über Mandanten hinweg sehen, mit ausgeschriebenem Grund. Braucht in Etappe 3 eine sichtbare Kennzeichnung (Systemkontext), aber keinen `forTenant()`-Umbau. - **`keine-mandantengebundene-tabelle`** — betrifft eines der acht Modelle ohne `tenantId` bzw. eine bewusst plattformweite Tabelle (D-03). Kein Umbau nötig. - **`beides`** — ein vierter, im Plan ausdrücklich verlangter Sonderfall: ein Hintergrunddienst, der zurecht über alle Mandanten hinweg eine Liste aufbaut (bewusst übergreifend), aber *innerhalb* der Schleife je Mandant binden muss (muss mandantengebunden werden). Beide Anteile sind in derselben Datei vorhanden; die Fundstelle bekommt hier eine Sammelklassifikation, die Aufteilung auf Zeilenebene steht in der Begründung. ## Zwei belegte Befunde **`req.tenantPrisma` wird gesetzt, aber nirgends gelesen.** `tenant.middleware.ts:44` und `tenant.guard.ts:41` setzen `req.tenantPrisma = forTenant(this.prisma, tenantId)`. Eine Volltextsuche über `apps/api/src` nach `tenantPrisma` außerhalb dieser beiden Dateien und ihrer Tests findet keine lesende Stelle — kein Controller greift darauf zu. Die Verdrahtung besteht, wird aber nicht genutzt. Für Etappe 2 ist zu entscheiden, ob die Controller künftig darüber gehen (dann bräuchte es keinen zweiten `forTenant()`-Aufruf je Service-Methode) oder ob der Weg ersatzlos entfällt. Dieser Plan entscheidet das nicht, hält den Befund nur fest. **WINDOWS #19 — nullbares `tenantId` bei `SearchProvider` und `TenderRssFeedSource`.** Beide Modelle tragen ein nullbares `tenantId` (`SearchProvider` für admin-gepflegte Vorgabe-Suchmaschinen, wobei laut 05-02-Entscheidung die tatsächlichen Vorgaben als Konstanten und nicht als DB-Zeilen mit `tenantId = NULL` geführt werden — die Spalte ist nullbar, ob es heute tatsächlich `NULL`-Zeilen gibt, ist damit eine offene Frage für Etappe 3, nicht eine hier beantwortete; `TenderRssFeedSource` für plattformweite RSS-Quellen wie den geseedeten `service.bund.de`-Feed, D-06). Die aktuelle Policy `"tenantId" = current_tenant_id()` vergleicht `NULL` nie gleich — nach dem Scharfschalten wären plattformweite Zeilen für JEDEN Mandanten unsichtbar, nicht nur für fremde. Das ist heute ohne Wirkung (Schalter aus, #18), muss aber in Etappe 3 zusammen mit den restlichen Fundstellen gelöst werden: die Policy braucht für den Lesezugriff `tenantId IS NULL OR tenantId = current_tenant_id()`, während Schreibzugriffe weiterhin einen Mandanten verlangen. ## Übersicht je Bereich (Zeilentreffer je Bereich, ungebunden vs. gebunden) **Wichtig, seit 260909-ipc (Aufgabe 3):** die Spalte "Ungebunden" zählt NUR noch `this.prisma.`-Rohtreffer — eine unveränderte Spaltenüberschrift über einer veränderten Bedeutung wäre die nächste stille Falle, seit ein Bereich (`ldap`) tatsächlich gebundene Zugriffe hat, die aus dieser Zählung verschwinden. Die neue Spalte "Gebunden" zählt daneben die `forTenant()`-gebundenen Rohtreffer (`tenantPrisma.`, Konvention dieses Codes — siehe `rls-access-inventory.spec.ts` für die allgemeinere, namensunabhängige Erkennung über die `const = forTenant(`-Zuweisungsform). Beide Spalten sind Rohtreffer (mehrere Vorkommen desselben Modells in derselben Datei zählen mehrfach), nicht (Datei, Modell)-Paare wie in der Bestandsaufnahme unten. Gemessen mit `grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/ | grep -v spec | wc -l` bzw. `grep -ro "tenantPrisma\.[a-zA-Z]*\." apps/api/src/ | grep -v spec | wc -l` am 2026-09-09, **nach** den Änderungen aus Aufgabe 2/3 dieses Plans (260909-ipc): | Bereich | Ungebunden | Gebunden | Hinweis | |---|---|---|---| | tenders | 62 | 0 | unverändert | | groups | 37 | 0 | unverändert | | ldap | 4 | 26 | **war 21/0** — Aufgabe 2/3 (260909-ipc) haben `ldap-config.service.ts` (5 Methoden) und `ldap.service.ts` (6 Methoden, 11 Abfragen) auf `forTenant()` umgestellt. Die 4 verbleibenden ungebundenen Treffer sind bewusst: `getAllActiveConfigs`/`onApplicationBootstrap` (Befund B) und `resolveEmailForWrite` (Befund A, T-IPC-04) | | dkv | 21 | 0 | unverändert | | user | 17 | 0 | unverändert | | module-registry | 17 | 0 | unverändert | | dashboard | 13 | 0 | unverändert | | auth | 8 | 5 | unverändert gegenüber dem in Etappe 1 (260909-eor) gemessenen Stand | | calendar | 12 | 0 | unverändert | | tenant | 8 | 0 | unverändert | | favorites | 7 | 0 | unverändert | | settings | 4 | 0 | unverändert | | **Summe** | **210** | **31** | Ungebunden: war 227 vor dieser Etappe (260909-eor-Stand), Delta = die 17 in Aufgabe 2/3 umgestellten `ldap`-Rohtreffer. Gebunden: war 5 (nur `auth`), jetzt zusätzlich 26 in `ldap` | ## Klassen-Verteilung (nach (Datei, Modell)-Fundstellen, 61 Paare) Stand 260909-ipc (Aufgabe 2): 59 Paare aus der urspruenglichen Zaehlung plus zwei bisher unentdeckte, weil bereits gebundene Fundstellen (`auth.service.ts`/`passwordResetToken`, `ldap.service.ts`/`groupMembership`), die erst die um gebundene Zugriffe erweiterte Erkennung (Befund G) sichtbar macht — sie waren nie Teil der 227 `this.prisma.*`-Rohtrefferzahl, weil sie schon vor diesem Plan über `forTenant()` liefen. Dazu die Korrektur von (`ldap-config.service.ts`, `ldapConfig`) von `muss-mandantengebunden` auf `beides` (Befund B). | Klasse | Anzahl Paare | |---|---| | muss-mandantengebunden | 32 | | keine-mandantengebundene-tabelle | 16 | | beides | 10 | | bewusst-uebergreifend | 3 | | **Summe** | **61** | ## Der Hintergrunddienst als Falle — drei `beides`-Fälle Ein Planer, der über alle Mandanten iteriert, liest zu Recht übergreifend — muss aber *innerhalb* der Schleife je Mandant binden. Drei Dateien sind betroffen: - **`ldap.service.ts`** (AD-Abgleich) — **Stand 260909-ipc, Aufgaben 2/3: geschlossen.** Iteriert nicht selbst über alle Mandanten (der Sync läuft je Aufruf für einen übergebenen Mandanten). Vor dieser Etappe blieben Lesezugriffe auf `group`, `ldapConfig` und `user` innerhalb der Sync-Methoden teils ungebunden, obwohl der Mandant zu diesem Zeitpunkt bereits bekannt war — nur 4 der inzwischen 11 `forTenant()`-Aufrufstellen deckten die Lese-/Schreibpfade ab. Jetzt sind `group` und `ldapConfig` vollständig gebunden; bei `user` bleibt ausschließlich `resolveEmailForWrite` bewusst ungebunden (Befund A, T-IPC-04 — siehe Bestandsaufnahme unten). Der als gefährlichster Punkt benannte Löschzweig (`syncBoundGroupsForTenant`, WINDOWS #20) war bereits seit Etappe 1 gebunden. Offen bleibt eine Reihenfolgebedingung für Etappe 4, NICHT Teil dieser Umstellung: die Übergabe unmittelbar vor der Löschung — `this.groupsService.reassignDefaultBeforeDelete(tenantId, group.id)` und `ensureDefaultGroup(tenantId)` — liegt in `groups.service.ts` und ist nicht gebunden. Nach dem Scharfschalten würde `reassignDefaultBeforeDelete` still `false` melden (kein Ersatzkandidat sichtbar), der Standard-Marker wandert nicht mit, und der Mandant bliebe nach einer Gruppenlöschung ohne Standardgruppe zurück — der Bereich `groups` muss deshalb vor Etappe 4 umgestellt sein (siehe `docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt (e), Befund D). - **`tender-digest.scheduler.ts`** (Ausschreibungs-Digest): liest `tenderMatch`/`tenderNotificationPref`/`user` bewusst über ALLE Mandanten in einem `findMany` (ein einziger globaler Cron-Job, kein Mandant im Job selbst — so von Anfang an entworfen, Pitfall 1 in den Kommentaren der Datei). Innerhalb der Verteilung je Treffer ist der Mandant aus der Zeile bekannt und der Versand muss darauf gebunden laufen. - **`tender-matching.service.ts`** (Ausschreibungs-Sofortmeldung): dieselbe Form — `tenderMatch`/`tenderSavedSearch`/`user` werden für die Sofort-Benachrichtigung über alle betroffenen Mandanten hinweg gelesen, der Versand je Treffer ist mandantengebunden. ## Bestandsaufnahme Maschinell ermittelt, `rls-access-inventory.spec.ts` hält Vollständigkeit nach. Spalten: Datei, Modell (Prisma-Modellname wie in `this.prisma.` oder — seit 260909-ipc, Befund G — in `.` verwendet), Klasse, Stand (`gebunden`/`ungebunden`/`gemischt`, seit 260909-ipc maschinell gegen den Quelltext geprüft), Begründung. | Datei | Modell | Klasse | Stand | Begründung | |---|---|---|---|---| | apps/api/src/auth/auth.service.ts | passwordResetToken | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS ueber Join auf `User` (Migration 20260618112133). `requestPasswordReset`/`resetPassword` laufen vollstaendig ueber `forTenant()` (Etappe 1, WINDOWS #20, Aufgabe 1) — von der alten, nur `this.prisma.*` erkennenden Suche nie erfasst, weil bereits gebunden; die erweiterte Erkennung aus Aufgabe 2 (260909-ipc) macht diese Fundstelle erstmals sichtbar. | | apps/api/src/auth/auth.service.ts | user | muss-mandantengebunden | gemischt | `getMe`, `changePassword`, `adminResetPassword` suchen über die Benutzerkennung aus dem Sitzungsnachweis — der Mandant ist dort bereits bekannt (Aufgabe 2 fasst sie bewusst nicht an, siehe SUMMARY). | | apps/api/src/calendar/calendar.service.ts | calendarSource | muss-mandantengebunden | ungebunden | Kalenderquellen eines Nutzers, `tenantId`-Spalte vorhanden. | | apps/api/src/dashboard/dashboard.service.ts | dashboardLayout | muss-mandantengebunden | ungebunden | Widget-Anordnung eines Nutzers, `tenantId`-Spalte vorhanden. | | apps/api/src/dashboard/dashboard.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit, kein `tenantId` (Migration 20260909140000, Gruppe b). | | apps/api/src/dashboard/dashboard.service.ts | searchProvider | muss-mandantengebunden | ungebunden | `tenantId` nullbar (WINDOWS #19) — heutige, tatsächlich gespeicherte Zeilen sind nutzerangelegt und tragen einen Mandanten; Vorgabe-Anbieter kommen laut 05-02 aus Konstanten, nicht aus der DB. | | apps/api/src/dashboard/dashboard.service.ts | widgetInstance | muss-mandantengebunden | ungebunden | Platzierte Dashboard-Widgets eines Nutzers, `tenantId`-Spalte vorhanden. | | apps/api/src/dkv/dkv.service.ts | dkvInvoiceHistory | muss-mandantengebunden | ungebunden | DKV-Rechnungshistorie je Mandant, `tenantId`-Spalte vorhanden. | | apps/api/src/dkv/dkv.service.ts | dkvModuleConfig | muss-mandantengebunden | ungebunden | Postfach-/Zugangsdaten des DKV-Moduls je Mandant. | | apps/api/src/dkv/dkv.service.ts | dkvVehicleMaster | muss-mandantengebunden | ungebunden | Fahrzeugstammdaten des DKV-Moduls je Mandant. | | apps/api/src/favorites/favorites.service.ts | favoriteLink | muss-mandantengebunden | ungebunden | Favoriten-Links eines Nutzers, `tenantId`-Spalte vorhanden. | | apps/api/src/groups/groups.service.ts | group | muss-mandantengebunden | ungebunden | Gruppen sind je Mandant, `tenantId`-Spalte vorhanden. | | apps/api/src/groups/groups.service.ts | groupMembership | muss-mandantengebunden | ungebunden | Kein eigenes `tenantId`, RLS über Join auf `Group` (Migration 20260618112133-Nachfolger) — braucht trotzdem `forTenant()`, damit der Join-Kontext gesetzt ist. | | apps/api/src/groups/groups.service.ts | moduleGrant | muss-mandantengebunden | ungebunden | Modulfreigaben je Mandant, `tenantId`-Spalte vorhanden. | | apps/api/src/groups/groups.service.ts | user | muss-mandantengebunden | ungebunden | Nutzerverwaltung innerhalb eines Mandanten. | | apps/api/src/groups/module-grants.service.ts | group | muss-mandantengebunden | ungebunden | Wie groups.service.ts. | | apps/api/src/groups/module-grants.service.ts | groupMembership | muss-mandantengebunden | ungebunden | Kein eigenes `tenantId`, RLS über Join auf `Group`. | | apps/api/src/groups/module-grants.service.ts | moduleGrant | muss-mandantengebunden | ungebunden | Modulfreigaben je Mandant. | | apps/api/src/groups/module-grants.service.ts | tenantModuleActivation | muss-mandantengebunden | ungebunden | Welche Module ein Mandant aktiviert hat, `tenantId`-Spalte vorhanden. | | apps/api/src/groups/module-grants.service.ts | user | muss-mandantengebunden | ungebunden | Zielbenutzer eines Grants innerhalb des Mandanten. | | apps/api/src/ldap/ldap-config.service.ts | ldapConfig | beides | gemischt | `getConfig`, `createConfig` und `updateConfig` laufen ueber `forTenant()`, gebunden an den aus der Anfrage bereits bekannten Mandanten. `getAllActiveConfigs()` (Planer-Lesezugriff ueber ALLE Mandanten) und die Start-Nachverschluesselung in `onApplicationBootstrap` bleiben bewusst uebergreifend: beide laufen, bevor bzw. unabhaengig davon, ob ein einzelner Mandantenkontext feststeht (Befund B, 260909-ipc-PLAN.md). Korrektur der Klasse von `muss-mandantengebunden`: das Paar ist tatsaechlich `beides`, keine Verhaltensaenderung. | | apps/api/src/ldap/ldap-config.service.ts | ldapFieldMapping | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS ueber Join auf `LdapConfig`. `addFieldMapping` und `removeFieldMapping` nehmen den Mandanten jetzt als Parameter entgegen und laufen vollstaendig ueber `forTenant()` (T-IPC-01, 260909-ipc-PLAN.md) — schliesst zugleich die Fremdzugriffsluecke beim Loeschen einer Feldzuordnung ueber ihre Kennung. | | apps/api/src/ldap/ldap.service.ts | group | beides | gebunden | AD-Abgleich: `listGroups` (die "bereits importiert"-Markierung) und `importGroupsByDn` (die Idempotenzpruefung ueber `ldapObjectGuid`) sind mit Aufgabe 3 (260909-ipc) auf `forTenant()` umgestellt — zusammen mit den bereits vorher gebundenen Stellen (Anlage, Mitgliedschafts- und Gruppenabgleich) ist damit jeder `group`-Zugriff dieser Datei gebunden. Die Klasse bleibt `beides`, weil ein zukuenftiger uebergreifender Lesezugriff (z. B. ein neuer Planer-Pfad) hier ebenso legitim waere wie bei `ldapConfig` unten — nicht, weil heute noch ein ungebundener Zugriff bestuende. | | apps/api/src/ldap/ldap.service.ts | groupMembership | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS ueber Join auf `Group`. `syncGroupMembershipsForTenant` laeuft vollstaendig ueber `forTenant()` (Plan 16-03/16-05) — von der alten, nur `this.prisma.*` erkennenden Suche nie erfasst, weil bereits gebunden; die erweiterte Erkennung aus Aufgabe 2 (260909-ipc) macht diese Fundstelle erstmals sichtbar. | | apps/api/src/ldap/ldap.service.ts | ldapConfig | beides | gebunden | Die `lastSyncAt`-Fortschreibung am Ende von `syncUsersForTenant` ist mit Aufgabe 3 (260909-ipc) auf den in derselben Methode bereits vorhandenen `forTenant()`-Client umgestellt — es entsteht kein zweiter. Damit ist der einzige `ldapConfig`-Zugriff dieser Datei gebunden. | | apps/api/src/ldap/ldap.service.ts | user | beides | gemischt | Mit Aufgabe 3 (260909-ipc) sind `upsertMappedUser` (Identitaetssuche und Aktualisierung), `searchUsers` (die "bereits importiert"-Markierung), `importUsersByDn` (Dedup und ldapDn-Nachtrag) und die Deaktivierungsschleife in `syncUsersForTenant` auf `forTenant()` umgestellt. `resolveEmailForWrite` bleibt ausdruecklich UNGEBUNDEN (Befund A, T-IPC-04): `email`/`username` sind plattformweit eindeutig, eine mandantengebundene Suche saehe einen fremden Halter nicht mehr und meldete faelschlich "frei" — die geloeste Klasse waere `muss-mandantengebunden` gewesen, bleibt wegen dieser einen bewusst uebergreifenden Abfrage `beides`. Der Loeschzweig um `syncBoundGroupsForTenant` (WINDOWS #20) ist bereits seit Etappe 1 gebunden und war nie Teil dieses Befunds. | | apps/api/src/module-registry/module-access.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit, kein `tenantId`. | | apps/api/src/module-registry/module-access.service.ts | moduleGrant | muss-mandantengebunden | ungebunden | Modulfreigaben je Mandant. | | apps/api/src/module-registry/module-access.service.ts | tenantModuleActivation | muss-mandantengebunden | ungebunden | Aktivierung je Mandant, `tenantId`-Spalte vorhanden. | | apps/api/src/module-registry/module-registry.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit. | | apps/api/src/module-registry/module-registry.service.ts | tenantModuleActivation | muss-mandantengebunden | ungebunden | Aktivierung je Mandant. | | apps/api/src/settings/settings.service.ts | smtpConfig | muss-mandantengebunden | ungebunden | SMTP-Zugangsdaten je Mandant, `tenantId`-Spalte vorhanden. | | apps/api/src/tenant/tenant.controller.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | `Tenant` ist die Mandantentabelle selbst — hat keine eigene `tenantId`-Spalte, kann sie per Definition nicht haben (Migration 20260909140000, Gruppe b). | | apps/api/src/tenant/tenant.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Dieselbe Begründung. | | apps/api/src/tenders/adapters/email-alert.adapter.ts | tenderEmailConfig | bewusst-uebergreifend | ungebunden | `fetchTenders()` liest bewusst jede aktive `TenderEmailConfig`-Zeile über ALLE Mandanten in einer Abfrage (Plattform-Scheduler, ein Tick pro Postfach, D-13/D-01) — ausführlich im Dateikopf begründet, darf laut Kommentar niemals in `forTenant()` verpackt werden. | | apps/api/src/tenders/adapters/rss.adapter.ts | tenderRssFeedSource | bewusst-uebergreifend | ungebunden | Fan-out über jeden aktiven Feed, plattformweit UND persönlich, in einer Abfrage (Zeilen 55–83 im Dateikopf begründet) — dieselbe Scheduler-Ebene wie beim E-Mail-Adapter. | | apps/api/src/tenders/tender-dedup.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Explizit im Dateikopf: "platform-global, RLS-exempt tables. Never wrap these queries in forTenant()." (D-03) | | apps/api/src/tenders/tender-dedup.service.ts | tenderSource | keine-mandantengebundene-tabelle | ungebunden | Dieselbe Begründung. | | apps/api/src/tenders/tender-digest.scheduler.ts | tenderMatch | beides | ungebunden | Ein einziger globaler Cron-Job liest über ALLE Mandanten (bewusst übergreifend, Pitfall-1-Kommentar im Dateikopf), der Versand je Treffer ist an dessen Mandanten gebunden. | | apps/api/src/tenders/tender-digest.scheduler.ts | tenderNotificationPref | beides | ungebunden | Dieselbe Begründung — Präferenzen werden über alle Mandanten gelesen, aber je Zeile mandantenbezogen ausgewertet. | | apps/api/src/tenders/tender-digest.scheduler.ts | user | beides | ungebunden | E-Mail-Adressen für den Versand werden über alle Mandanten gelesen, der eigentliche Versand ist je Treffer mandantengebunden. | | apps/api/src/tenders/tender-email-config.service.ts | tenderEmailConfig | muss-mandantengebunden | ungebunden | Nutzer-CRUD für die eigene Postfachanbindung (Phase 17, D-01) — anders als der Fan-out-Adapter oben, hier ist der Mandant aus der Anfrage bekannt. | | apps/api/src/tenders/tender-fingerprint-backfill.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Einmaliges Backfill-Skript über den plattformweiten `Tender`-Katalog (D-03). | | apps/api/src/tenders/tender-ingestion.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Explizit im Dateikopf: "Multi-tenant safety (D-03, T-10-09): uses the plain, non-tenant-scoped ... queries ... these are platform-global". | | apps/api/src/tenders/tender-ingestion.service.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter Poll-Status, kein `tenantId` (Migration 20260909140000, Gruppe b). | | apps/api/src/tenders/tender-matching.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Liest den plattformweiten Katalog (D-03), um Treffer zu berechnen — kein `tenantId`. | | apps/api/src/tenders/tender-matching.service.ts | tenderMatch | beides | ungebunden | Sofortmeldung: Treffer über alle betroffenen Mandanten gelesen, Versand je Treffer mandantengebunden — siehe Abschnitt "Der Hintergrunddienst als Falle". | | apps/api/src/tenders/tender-matching.service.ts | tenderSavedSearch | beides | ungebunden | Dieselbe Begründung — gespeicherte Suchprofile aller Mandanten werden gegen neue Treffer geprüft, der Versand ist je Profil mandantengebunden. | | apps/api/src/tenders/tender-matching.service.ts | user | beides | ungebunden | Dieselbe Begründung — E-Mail-Adressen für die Sofortmeldung. | | apps/api/src/tenders/tender-notification-pref.service.ts | tenderNotificationPref | muss-mandantengebunden | ungebunden | Nutzer-CRUD für die eigenen Benachrichtigungseinstellungen — Mandant aus der Anfrage bekannt. | | apps/api/src/tenders/tender-rss-feed.service.ts | tenderRssFeedSource | muss-mandantengebunden | ungebunden | Nutzer-CRUD für die eigenen RSS-Quellen (anders als der Fan-out in `adapters/rss.adapter.ts`) — Mandant aus der Anfrage bekannt. WINDOWS #19 betrifft die nullbaren plattformweiten Zeilen, nicht diesen CRUD-Pfad. | | apps/api/src/tenders/tender-saved-search.service.ts | tenderSavedSearch | muss-mandantengebunden | ungebunden | Nutzer-CRUD für die eigenen gespeicherten Suchprofile — Mandant aus der Anfrage bekannt. | | apps/api/src/tenders/tender-scheduler.service.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter DÖE-Poll-Status, kein `tenantId` — im Dateikopf explizit als "genuine platform-wide singleton" begründet. | | apps/api/src/tenders/tender-triage.service.ts | tenderTriage | muss-mandantengebunden | ungebunden | Favorisierungs-/Ablehnungsstatus eines Nutzers, `tenantId`-Spalte vorhanden. | | apps/api/src/tenders/tenders.controller.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Lesezugriff auf den plattformweiten Katalog (D-03). | | apps/api/src/tenders/tenders.controller.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter Poll-Status, admin-verwaltet, kein `tenantId`. | | apps/api/src/tenders/tenders.module.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Singleton-Bestückung beim Boot — im Dateikopf explizit als "global, RLS-exempt (D-03)" begründet. | | apps/api/src/user/admin-seed.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Legt beim ersten Start den Standard-Mandanten selbst an — `Tenant` hat keine `tenantId`-Spalte. | | apps/api/src/user/admin-seed.service.ts | user | bewusst-uebergreifend | ungebunden | Erstanlage des Administrators beim ersten Start: läuft einmalig beim Boot, BEVOR irgendein Mandantenkontext existiert, um den allerersten Mandanten samt Admin-Nutzer anzulegen — es gibt zu diesem Zeitpunkt strukturell keinen Mandanten, an den gebunden werden könnte. | | apps/api/src/user/user.controller.ts | user | muss-mandantengebunden | ungebunden | Nutzerverwaltung innerhalb des Mandanten des anfragenden Admins. | | apps/api/src/user/user.service.ts | user | muss-mandantengebunden | ungebunden | Dieselbe Begründung. | ## Was diese Etappe NICHT entscheidet - Ob Controller künftig über `req.tenantPrisma` statt eines erneuten `forTenant()`-Aufrufs im Service gehen (offener Befund oben). Der Bereich `ldap` (260909-ipc) hat sich für den Dienst-internen Weg entschieden — `forTenant(this.prisma, tenantId)` wird in jeder umgestellten Methode neu erzeugt, wie es die vier Bestandsstellen in `ldap.service.ts` und die drei in `auth.service.ts` bereits vormachten. Die Frage bleibt für alle übrigen Bereiche der Etappe 2 offen. - Wie die WINDOWS-#19-Policy für `SearchProvider`/`TenderRssFeedSource` am Ende genau lautet — nur, dass sie vor dem Scharfschalten gelöst sein muss. - Die Reihenfolge und Zuschnitt der Etappe-2-Pläne — dafür ist die Klassen-Verteilung oben der Arbeitsvorrat, siehe `` im Plan `260909-eor-PLAN.md`.