# Architektur ## Dienste ``` Browser └─ cloudflared (Tunnel, TLS und Kompression liegen bei Cloudflare) └─ web Next.js 15, App Router, Server Actions, SSE-Endpunkt ├─ pgbouncer ─ db PostgreSQL 16 ├─ files Volume, getrennt in quarantine/ und store/ ├─ clamav clamd, angesprochen über Unix-Socket └─ pdf Playwright-Container für PDF-Erzeugung worker discord.js, hängt am Gateway, schreibt Rollen in db db-backup Dauerläufer mit Cron, nächtlicher Dump offsite-backup Dauerläufer mit Cron, restic auf zweites Ziel ``` Alles liegt in **einem** Dockge-Stack. Kein Reverse Proxy im Stack: der Tunnel terminiert TLS, Cloudflare komprimiert, und Next.js liefert seine statischen Dateien selbst mit korrekten Cache-Headern aus. Am Router wird kein Port geöffnet. Migrationen laufen im Startkommando von `web`, nicht in einem Init-Container. Begründung und die vollständige `compose.yaml` in `docs/11-betrieb.md`. ## Warum ein eigener Worker Die Web-App fragt nie live bei Discord an. Der Worker hält `GuildMember` in Postgres aktuell: - Gateway-Ereignisse `guildMemberUpdate`, `guildMemberRemove`, `guildMemberAdd`, `roleUpdate` - Voller Abgleich alle 5 Minuten als Sicherheitsnetz - Bei jeder Änderung: `pg_notify('member_changed', userId)`, damit die Web-App laufende Sessions invalidieren kann Dadurch ist eine Rechteänderung in Discord in unter einer Sekunde in der App wirksam, ohne dass ein einziger Request in der App auf die Discord-API warten muss. ## Verzeichnisstruktur ``` . ├─ app/ │ ├─ (auth)/ Login, Callback, Fehlerseiten │ ├─ (app)/ │ │ ├─ dashboard/ │ │ ├─ faelle/ │ │ │ ├─ page.tsx Liste │ │ │ ├─ loading.tsx Skelett │ │ │ ├─ neu/ │ │ │ └─ [caseNumber]/ │ │ │ ├─ page.tsx Akte │ │ │ ├─ loading.tsx │ │ │ └─ @chat/ Paralleler Slot, lädt unabhängig │ │ ├─ personal/ │ │ ├─ suche/ │ │ └─ admin/ Rollen-Mapping, Vorlagen, Protokoll │ └─ api/ │ ├─ auth/[...nextauth]/ │ ├─ files/[id]/ Ausliefernder Endpunkt mit Rechteprüfung │ ├─ upload/ │ └─ stream/ SSE ├─ lib/ │ ├─ auth/ Session, Rechteauflösung │ ├─ access/ Zugriffsprüfung als Prisma-Where-Fragmente │ ├─ db.ts │ ├─ audit.ts │ ├─ i18n/de.ts │ └─ realtime/ ├─ components/ ├─ worker/ Eigenes Paket, discord.js ├─ prisma/ ├─ docs/ └─ docker/ ``` ## Datenflüsse, die man kennen muss **Seitenaufruf einer Akte.** Server Component lädt Kopfdaten und Abschnitte in einer Abfrage. Verlauf und Chat hängen in eigenen ``-Grenzen und streamen nach. Die Seite ist sichtbar, bevor der Verlauf da ist. **Neuer Vermerk.** Server Action schreibt `CaseEntry`, danach `revalidateTag('case:')` und `pg_notify` für alle offenen SSE-Verbindungen dieser Akte. Der Client hat den Eintrag schon optimistisch angezeigt und ersetzt ihn beim Eintreffen anhand der Client-ID. **Upload.** Datei geht per XHR mit Fortschritt an `/api/upload`, landet in `quarantine/`, Prüfkette läuft asynchron, Statuswechsel kommt über SSE zurück. **Rechteänderung in Discord.** Worker schreibt, `pg_notify`, Web-App verwirft den Cache der betroffenen Session, nächster Request rechnet die Stufe neu.