# Aufbau des Codes Dieses Dokument beschreibt den **aktuell implementierten** Stand. Die angestrebte Gesamtarchitektur steht in [SYNCOVA_ARCHITECTURE.md](../SYNCOVA_ARCHITECTURE.md). ## Schichtung ```text apps/web Weboberfläche (React + TypeScript) | HTTP (gleicher Ursprung, im Entwicklungsbetrieb über Proxy) v apps/api Control-Plane-API (Go) | v packages/platform Querschnittsdienste: config, logging, database, health | v PostgreSQL ausschließlich Control Plane, niemals Backup-Nutzdaten ``` Die Abhängigkeitsrichtung ist strikt: `packages/` kennt `apps/` nicht. Damit können Agent und Worker später dieselben Querschnittsdienste nutzen, ohne die API einzubinden. ## packages/platform | Paket | Verantwortung | | --- | --- | | `config` | Konfiguration aus Umgebungsvariablen, vollständige Validierung | | `logging` | strukturiertes Logging, Correlation IDs, Secret-Redaction | | `database` | PostgreSQL-Pool, Health-Check, Migrationsverwaltung | | `health` | Health Engine über alle Komponenten | ### config Alle Werte stammen aus `SYNCOVA_*`-Umgebungsvariablen. Zwei Entscheidungen prägen das Paket: - **Secrets besitzen keinen Standardwert.** Das Datenbankpasswort ist als nicht exportiertes Feld abgelegt, damit es nicht versehentlich serialisiert wird. Zugriff besteht nur über `ConnectionString()` (mit Passwort) und `RedactedConnectionString()` (ohne). - **Validierungsfehler werden gesammelt.** `Load()` meldet alle Probleme gemeinsam, statt beim ersten abzubrechen. Sicherheitsrelevante Voreinstellungen: Listener nur auf `127.0.0.1`, CORS leer, Body-Limit 1 MiB, Wildcard-Herkunft grundsätzlich unzulässig, unverschlüsselte Herkunft in der Produktion abgelehnt. ### logging `slog` mit JSON-Ausgabe. Die Redaction hängt an `ReplaceAttr` und greift damit für **jedes** Attribut — der einzige Ort, an dem sie nicht vergessen werden kann. Erkannt werden Feldnamen, die auf ein Geheimnis hindeuten (`password`, `token`, `secret`, `connection_string` und weitere), unabhängig von Schreibweise und Präfix. ### health Jede Komponente meldet einen expliziten Zustand; der Gesamtzustand ist der schlechteste Einzelzustand. Drei Eigenschaften sind bewusst so gebaut: - Eine Prüfung ohne gemeldeten Zustand gilt als **kritisch**, nicht als gesund. - Eine Zeitüberschreitung gilt als kritisch, statt den Endpunkt zu blockieren. - Ein Panic in einer Prüfung wird abgefangen und als kritisch gemeldet. Nur als kritisch registrierte Komponenten beeinflussen die Readiness. Eine ausgefallene Nebenkomponente nimmt den Dienst also nicht aus dem Verkehr, erscheint aber weiterhin ehrlich im Bericht. ## apps/api ### Middleware-Reihenfolge ```text Recovery → Correlation → SecurityHeaders → CORS → BodyLimit → AccessLog → Router ``` Recovery liegt außen, damit auch Fehler innerer Schichten erfasst werden. Correlation folgt unmittelbar, damit jede weitere Schicht bereits eine ID zum Loggen hat. ### Request-ID und Correlation ID Die **Request-ID** wird immer serverseitig als UUID vergeben. Die **Correlation ID** darf der Aufrufer über `X-Correlation-ID` vorgeben, um eine Operation über Systemgrenzen zu verfolgen — aber nur, wenn der Wert eine gültige UUID ist. Andernfalls wird er verworfen, damit keine fremden Zeichenketten in die Logs gelangen. ### Antworthülle ```json { "data": {}, "meta": { "request_id": "uuid" } } { "error": { "code": "...", "message": "...", "request_id": "uuid" } } ``` `APIError` trennt die ausgelieferte Darstellung von der internen Ursache: Letztere wird über `WithCause` mitgeführt, erscheint im Log und **nie** in der Antwort. Maßgeblich für den Client ist die Hülle, nicht der HTTP-Status. Beide tragen unterschiedliche Aussagen: der Status beschreibt den Betriebszustand, die Hülle den Inhalt. `GET /api/v1/health` nutzt genau das und meldet einen kritischen Systemzustand mit **503**, liefert dabei aber den vollständigen Bericht als Nutzlast — das externe Monitoring schlägt an, und die Oberfläche kann trotzdem anzeigen, *was* kaputt ist. ### Health-Endpunkte | Endpunkt | Prüft | Zweck | | --- | --- | --- | | `GET /health/live` | nur den Prozess | Neustartentscheidung | | `GET /health/ready` | alle kritischen Komponenten | Verkehrszuteilung | | `GET /api/v1/health` | alle Komponenten einzeln | Diagnose | Liveness prüft bewusst **keine** Abhängigkeiten: sonst würde eine kurzzeitig nicht erreichbare Datenbank einen Neustart des Dienstes auslösen und das Problem verschlimmern. ## Datenbank PostgreSQL ist die Control-Plane-Datenbank und enthält niemals Backup-Nutzdaten. Beide Zugriffswege — Verbindungspool und Migrationsläufe — nutzen denselben `pgx`-Treiber. Das ist eine bewusste Festlegung: `golang-migrate` würde sonst seinen eingebauten `lib/pq`-Treiber verwenden, der andere SSL-Modi kennt, womit dieselbe Konfiguration je nach Kommando funktionieren oder scheitern würde. Migrationen sind über `embed` ins Binary eingebettet und laufen ausschließlich über `syncova-migrate`. Der API-Dienst prüft beim Start nur, ob der Schemastand zur Programmversion passt. ## apps/web Aufbau nach PROMPT.md §104: ```text src/ api/ HTTP-Client mit Fehlerbehandlung components/ wiederverwendbare Bausteine des Designsystems features/ fachliche Bereiche (aktuell: health) styles/ Design-Token types/ Typen des API-Vertrags ``` Der Client `requestApi` kapselt die Antworthülle und liefert Fehler stets als `ApiError`. Eine Antwort außerhalb des Vertrags führt zu einem klaren Fehler statt zu einem stillschweigend leeren Ergebnis. Die Statusfarben (grün/gelb/orange/rot/blau) sind ausschließlich Zuständen vorbehalten. Ein Zustand wird nie allein über Farbe vermittelt, sondern immer zusätzlich als Text — Farbe allein wäre für farbfehlsichtige Anwender unzugänglich.