# Getroffene Entscheidungen Stellen, an denen die Fachdokumente sich widersprechen, lückenhaft sind oder gegen eine Eigenschaft von Postgres laufen. Jede Zeile nennt den Konflikt, die Entscheidung und den Grund. Wo eine Entscheidung fachlich anders ausfallen kann, steht das ausdrücklich dabei. ## Struktur | Konflikt | Entscheidung | |---|---| | `CLAUDE.md` und `README.md` verweisen auf `docs/`, die Dateien lagen im Wurzelverzeichnis | Nach `docs/` verschoben | | `02-architektur.md` zeigt `app/` und `lib/` im Wurzelverzeichnis, `TASKS.md` Phase 0 verlangt einen pnpm-Workspace mit `web/` und `worker/` | Workspace mit `web/`, `worker/`, `pdf/` und `packages/db/`. Die Struktur aus `02-architektur.md` gilt innerhalb von `web/` | | Der Worker braucht denselben Prisma-Client wie die Web-App | Eigenes Workspace-Paket `packages/db` (`@afis/db`), von `web` und `worker` benutzt. Migrationen und Seed liegen dort | ## Datenmodell | Konflikt | Entscheidung | |---|---| | `CaseParticipant` hat den zusammengesetzten Primärschlüssel `[caseId, role, userId, personnelFileId]`, aber `userId` und `personnelFileId` sind beide optional. Postgres lässt NULL in einem Primärschlüssel nicht zu | Eigener Schlüssel `id`, Eindeutigkeit über zwei **partielle** Unique-Indizes plus eine CHECK-Bedingung, dass mindestens eines der beiden Felder gesetzt ist | | `04-datenmodell.md` nennt zugleich `CREATE SEQUENCE case_number_2026` und die Tabelle `CaseCounter(year, lastNumber)` mit `SELECT … FOR UPDATE` | `CaseCounter`. Eine Sequenz pro Jahr müsste jedes Jahr von Hand angelegt werden, und der Abteilungsleiter ist ausdrücklich nicht technisch | | Das rohe SQL benutzt `search_vector` und `updated_at` in Schlangenschrift, die Prisma-Modelle Höckerschrift | Prisma-Voreinstellung behalten: Tabellen in Höckerschrift-Pascal, Spalten in Höckerschrift. Das SQL quotet entsprechend (`"searchVector"`, `"updatedAt"`) | | Kein Modell für Kanäle, obwohl `ChatMessage.channelId` existiert und `06-chat-und-realtime.md` Kanäle mit eigener Mindeststufe verlangt | Modell `Channel` ergänzt, dazu `ChatReaction` für den festen Reaktionssatz | | `/api/health` soll den Discord-Gateway prüfen, aber die Web-App fragt nie live bei Discord an | Modell `ServiceHeartbeat`. Der Worker schreibt alle 30 Sekunden seinen Zustand, `web` liest ihn | ## Der wichtigste Widerspruch `04-datenmodell.md` verlangt `CREATE RULE entry_version_no_update … DO INSTEAD NOTHING` auf `CaseEntryVersion`. `05-akten-und-vorlagen.md` verlangt unter „Umklassifizierung wirkt rückwirkend", dass beim Anheben von `minClearance` **alle bestehenden Versionen den neuen Wert erben**. Beides zusammen geht nicht: die Regel würde genau die Nachführung verhindern, die das Leck schließt. Aufgelöst durch einen Trigger statt einer Regel. `CaseEntryVersion` bleibt anhängend, aber `minClearance` darf nachgeführt werden, und nur nach oben. Jede andere Änderung wirft einen Fehler. Das erhält beide Absichten: der Text ist unveränderlich, die Klassifizierung wirkt rückwirkend. Ein zweiter Trigger schützt `CaseEntry.authorId`, `createdAt`, `caseId` und `type` auf Datenbankebene, nicht nur in der Anwendung. ## Suche `08-suche-und-export.md` beschreibt einen `search_vector` je Akte aus Titel, Abschnitten und Verlaufseinträgen. Ungeklärt bleibt, was mit Inhalten oberhalb der Stufe des Suchenden passiert. Ein gemeinsamer Vektor über alle Abschnitte würde einen Nutzer mit Stufe 1 einen Begriff finden lassen, der nur in einem als `[[c4]]…[[/c4]]` geschwärzten Bereich steht. Das verstößt gegen harte Regel 6. **Vorläufig aufgenommen wird nur, was jeder lesen darf, der die Akte überhaupt sehen darf:** Aktenzeichen und Titel (Gewicht A), Abschnitte und Verlaufseinträge mit `minClearance = 0` (B und C), Schwärzungs-Auszeichnungen vorher entfernt. Zurückgezogene Einträge fallen heraus. Das ist sicher, aber für hohe Stufen zu sparsam: ein Nutzer mit Stufe 5 findet geheime Inhalte nicht über die Volltextsuche. **Phase 7 muss das auflösen**, sinnvollerweise über getrennte Vektorspalten je Stufe. Diese Entscheidung ist bewusst konservativ getroffen, weil ein Leck hier schwerer zu bemerken ist als eine Lücke. ## Prisma 7 Die Dokumente nennen nur „Prisma + PostgreSQL 16". Eingesetzt wird Prisma 7, die aktuelle Fassung. Sie unterscheidet sich in drei Punkten, die den Aufbau betreffen: | Änderung | Folge hier | |---|---| | Verbindungsdaten stehen nicht mehr im Schema | `packages/db/prisma.config.ts`. Migrationen laufen über `DIRECT_DATABASE_URL`, also an pgbouncer vorbei | | Keine Rust-Query-Engine mehr, stattdessen Treiber-Adapter | `@prisma/adapter-pg` über `pg`. Der Treiber wird für `LISTEN/NOTIFY` ohnehin gebraucht, und die Images müssen keine Engine nachladen | | Neuer Generator `prisma-client` statt `prisma-client-js` | Der Client wird als ECMAScript-Module nach `packages/db/src/generated` erzeugt statt nach `node_modules/.prisma`. Nötig mit `importFileExtension = "js"`, sonst schreibt er endungslose Importe, die Node im ESM-Modus nicht auflöst | Der erzeugte Client ist ein Bauartefakt und liegt nicht im Repository. `pnpm --filter @afis/db run build` erzeugt ihn, die CI tut das vor lint, typecheck und test. Für `prisma migrate deploy` braucht es weiterhin eine Rust-Schema-Engine. Sie wird beim Bauen des Web-Images heruntergeladen und liegt unter `/app/migrator` im Image. Beim Containerstart wird nichts nachgeladen: ein Container, der nur startet, wenn gerade eine Registry erreichbar ist, wäre auf einem Homeserver die falsche Bauart. ## Betrieb | Konflikt | Entscheidung | |---|---| | `11-betrieb.md` verlangt „Jeder Dienst braucht einen Healthcheck", die abgedruckte `compose.yaml` hat für `db-backup` und `offsite-backup` keinen | Ergänzt. `db-backup` über seinen HTTP-Port 8080, `offsite-backup` über Cron-Prozess und `restic version` | | Das Startkommando `npx prisma migrate deploy` würde im Container aus dem Netz nachladen | Auf die im Image unter `/app/migrator` mitgelieferte Prisma-CLI umgestellt, zusammen mit Schema, Migrationsordner und Schema-Engine | | `pnpm deploy` zieht die optionalen Peers von `@prisma/client` mit ins Image (CLI, Studio, TypeScript, rund 240 MB) | `--no-optional` beim Bündeln. Zur Laufzeit wird nichts davon gebraucht | ## Protokollierung `09-performance-und-ux.md` verlangt eine Puffertabelle für Audit-Einträge, die alle zwei Sekunden gebündelt geschrieben wird, zugleich aber, dass schreibende Aktionen ihren Eintrag **in derselben Transaktion** erzeugen. Umgesetzt wird beides getrennt: schreibende Aktionen protokollieren synchron in der Transaktion, Lesezugriffe ab Stufe 3 laufen über einen Puffer. Der Puffer liegt im Prozessspeicher, nicht in einer Tabelle — eine Puffertabelle hätte dieselbe Verlustgefahr bei einem Absturz, aber zusätzliche Schreiblast. Kommt in Phase 2. ## Vorlagen im Seed `CLAUDE.md` beschreibt `pnpm db:seed` als „Testdaten inkl. Vorlagen", `TASKS.md` ordnet die vier Startvorlagen aber Phase 3 zu. Der Seed legt vorerst nur an, was die Anwendung zum Laufen braucht: die sechs Freigabestufen, die Änderungsrichtlinie und den Aktenzeichen-Zähler. Die Vorlagen kommen mit dem Vorlagen-Editor in Phase 3.