30 KiB
ApiCore – Architektur
Basiert auf docs/lastenheft.md. Diese Datei beschreibt, WIE die fachlichen Anforderungen technisch umgesetzt werden.
1. Backend – Modulschnitt (NestJS)
Ein NestJS-Modul pro fachlichem Bereich, Reihenfolge entspricht Baureihenfolge in TASKS.md:
| Modul | Zweck | Abhängig von |
|---|---|---|
AuthModule |
Login, Refresh, Passwort-Reset, JWT-Strategien | – |
UsersModule |
Benutzerverwaltung, Rollen | Auth |
AuditModule |
Audit-Log-Grundstruktur (Service AuditLogService, wird von anderen Modulen injiziert) |
Users |
ApiariesModule |
Bienenstände | Auth |
ColoniesModule |
Völker, Standortwechsel-Historie, Ableger/Vereinigung | Apiaries |
QueensModule |
Königinnen, Volk-Historie | Colonies |
InspectionsModule |
Durchsichten | Colonies |
TasksModule |
Aufgaben, polymorph verknüpft | – |
VarroaModule |
Varroamessungen | Colonies |
TreatmentsModule |
Behandlungen | Colonies |
FeedingsModule |
Fütterungen | Colonies |
HarvestsModule |
Ernten | Apiaries, Colonies |
ExtractionsModule |
Schleuderungen | Harvests |
HoneyBatchesModule |
Honigchargen, Honigmessungen | Extractions |
StorageModule |
Lagerbehälter, einfache Lagerverwaltung (StockItem/StockMovement) | HoneyBatches |
DashboardModule |
Aggregations-Endpunkte | alle o.g. |
SearchModule |
globale Suche, Filter-Bausteine | alle o.g. |
ExportModule |
CSV/JSON/PDF-Export | alle o.g. |
ErpExportModule |
Nur lesender Inventar-Spiegel für ERPNext (/api/v1/export/inventar/..., Recht erp-inventory:read), Vertrag in docs/erp-inventar-export.md |
Products, Bottling, ProductionBatches, Stock, Harvests |
Jedes Modul: *.controller.ts, *.service.ts, dto/, *.module.ts, *.spec.ts (Unit), test/*.e2e-spec.ts (Integration). Rollenprüfung über @Roles()-Decorator + RolesGuard, angewendet pro Endpunkt – niemals nur UI-seitig.
Alle Routen unter /api/v1/..., versioniert über NestJS URI Versioning. OpenAPI-Doku automatisch über @nestjs/swagger, erreichbar unter /api/v1/docs.
2. Frontend – Seitenstruktur (Next.js App Router)
app/
├── (auth)/login/
├── (app)/ geschütztes Layout mit Bottom-Nav (mobil) / Sidebar (Desktop)
│ ├── dashboard/
│ ├── apiaries/[id]/
│ ├── colonies/[id]/
│ ├── queens/[id]/
│ ├── inspections/new/
│ ├── tasks/
│ ├── varroa/new/
│ ├── treatments/new/
│ ├── feedings/new/
│ ├── harvests/new/
│ ├── honey-batches/[id]/
│ ├── storage/
│ ├── search/
│ └── settings/users/ nur Administrator
Zentrale Schnellaktion "+ Neu" (Abschnitt 22): permanent sichtbarer Floating-Button in der mobilen Bottom-Nav, öffnet Auswahl-Sheet (Durchsicht, Fütterung, Behandlung, Varroamessung, Aufgabe, Ernte, Volk, Königin) → direkt zum jeweiligen Schnellerfassungs-Formular, mobil möglichst 1-2 Taps bis zum ersten Eingabefeld.
Datenzugriff ausschließlich über TanStack Query gegen die Backend-API (kein direkter DB-Zugriff, kein Next.js-eigenes Datenbank-Layer) – erzwingt API-first auch praktisch.
3. Auth-Flow
- Login (
POST /api/v1/auth/login) mit E-Mail/Passwort → Access-Token (15 min, JWT HS256,iss=apicore,aud=apicore:access) + Refresh-Token (in DB als Hash, rotierend). Bei aktiver MFA zuerst ein Zwischen-Token (aud=apicore:mfa-challenge), das nie als Access-Token gilt. - Frontend hält Access-Token im Memory/React-Context, Refresh-Token als httpOnly-Cookie (
Secure,SameSite=Lax, Pfad/api/v1/auth). - Bei 401 automatischer Refresh über
POST /api/v1/auth/refresh, danach Retry. Rotation atomar; Wiederverwendung eines alten Tokens (nach 10 s Gnadenfrist für parallele Tabs) beendet alle Sitzungen. SpätestensSESSION_MAX_AGE(7 Tage) nach dem Login ist neu anzumelden. - Guards in dieser Reihenfolge:
ThrottlerGuard→JwtAuthGuard(Authentifizierung, Gültigkeit frisch aus der DB je Anfrage) →PermissionsGuard(Berechtigung, fail-closed). - Maschinen-Zugriff (Wabenhain) nur per API-Schlüssel mit Scope-Allowlist und optionaler IP-Allowlist, siehe Annahme 14 und Abschnitt 9.
4. Datenfluss / Rückverfolgbarkeit
Kernkette (Abschnitt 16, 29) relational über Fremdschlüssel, nicht über Textfelder:
Apiary → Colony → (Inspection | Treatment | Feeding | VarroaMeasurement)
Apiary/Colony → Harvest → Extraction → HoneyBatch → StorageContainer
HoneyBatch referenziert Extraction, Extraction referenziert n Harvests (m:n über ExtractionHarvest), Harvest referenziert n Colonys (m:n über HarvestColony) + eine Apiary. Damit ist "von welchem Volk stammt Charge X" eine reine Join-Abfrage, kein Freitext.
Historie statt Löschen: alle fachlichen Haupttabellen haben status/deactivatedAt statt Hard-Delete. Verschiebungen (Volk↔Stand) und Königinnenwechsel werden als eigene Historientabellen geführt (ColonyApiaryHistory, QueenColonyHistory), nicht durch Überschreiben des aktuellen Zustands.
5. Technische Leitentscheidungen (bestätigt, keine Abweichung)
- Validierung: class-validator/class-transformer (nicht Zod) – konsistent mit NestJS-Idiomatik und
@nestjs/swagger-Decorators, ein Schema-Ort statt zwei. - Ein gemeinsames Git-Repo im
ApiCore/-Root für Backend + Frontend (Monorepo-artig, aber zwei unabhängigepackage.json, kein Turborepo/Nx-Overhead im MVP). - Package-Manager: npm (in beiden Projekten, keine Vermischung mit pnpm/yarn).
- Backend-Tests: Jest (NestJS-Standard). Frontend-Tests: Vitest + React Testing Library.
6. Annahmen & offene Fragen
Fachliche Annahmen, getroffen weil das Lastenheft an der Stelle keine eindeutige Vorgabe macht. Bei Bedarf mit dem Fachbereich klären, aktuell aber Grundlage der Umsetzung:
-
HoneyMeasurement als MVP-Entität: Abschnitt 26 MUSS-Liste nennt Honigmessungen nicht explizit, Abschnitt 4.17 aber als eigenen Unterpunkt und Abschnitt 30 verlangt Wassergehalt schnell abrufbar. Da sie klein und für die Chargen-Kette relevant ist, wird sie als Teil des
HoneyBatchesModuleim MVP mitgebaut statt auf v1.x verschoben. -
Treatment auf Einzelvolk statt "Völkergruppe": Abschnitt 4.11 erlaubt "Volk oder Völkergruppe". MVP modelliert
Treatmentpro Volk (colonyIdPflichtfeld); eine Gruppenbehandlung wird im MVP als mehrere Treatment-Datensätze mit gleicherbatchNumber/Notiz erfasst statt eigener Gruppen-Entität. Spätere Migration kann eineTreatmentGroup-Entität nachziehen, falls das in der Praxis zu unhandlich ist. -
Lagerverwaltung MVP-Umfang: "grundlegende Lagerverwaltung" (Abschnitt 26 Punkt 12) wird als generisches
StockItem+StockMovement(Kategorie Honig/Verbrauchsmaterial/Imkereimaterial) umgesetzt. Das detaillierte Inventar mit Wartungsdaten (Abschnitt 4.22) bleibt v1.x/"SOLL", wie im Lastenheft vorgesehen. -
API-Client-Rolle im MVP: kein vollwertiger OAuth2/Client-Credentials-Server. Ein API-Client ist ein
Usermit RolleAPI_CLIENTund einem langlebigen Service-Refresh-Token; vollwertiges Scopes-/Client-Management ist SOLL/KANN. -
Backup-fähige Struktur: kein eigenes Backup-Tooling im Code. Umgesetzt als: reproduzierbare Prisma-Migrationen (kein manuelles Schema-Drift), dokumentierte
pg_dump/PITR-Strategie in diesem Dokument (Betrieb obliegt dem bestehenden Cluster/DBA),/health-Endpoint für Betriebsüberwachung. -
Deviations vom Leitentscheidungs-Rahmen: bisher keine. Wird ergänzt, falls während der Umsetzung ein konkreter Grund für eine Abweichung von NestJS/Prisma/Next.js/Tailwind/shadcn/TanStack Query auftritt.
-
Dokumentenspeicher als lokales Volume (Modul 29):
DocumentsModulespeichert Uploads unterhalb vonDOCUMENTS_STORAGE_PATHauf dem lokalen Dateisystem des Backend-Pods, keine Objektspeicher-Anbindung (S3-kompatibel o.ä.) im MVP/v1.x. Der Pfad ist über die ConfigMap austauschbar, wie in Abschnitt 27 des Lastenhefts vorgesehen. Wichtig für den Betrieb: das aktuelledeploy/k8s-Deployment läuft mitreplicas: 2ohne geteiltes Volume – für konsistente Uploads über beide Pods hinweg muss vor Produktivbetrieb mit Dokumenten-Feature einReadWriteMany-PVC (z.B. NFS- oder EFS-basiert) gemountet undDOCUMENTS_STORAGE_PATHdarauf gesetzt werden, sonst sieht nur der Pod, der die Datei entgegengenommen hat, sie auch beim Download wieder. -
Entra ID SSO ohne Auto-Provisioning:
POST/GET /api/v1/auth/sso/*implementiert Authorization Code + PKCE gegen Entra ID überopenid-client. Der E-Mail-Claim aus dem ID-Token wird ausschließlich gegen einen bereits existierenden, aktivenUser-Datensatz gematcht – es wird kein neuer Benutzer automatisch angelegt (Just-in-Time-Provisioning), passend zum bestehenden Modell, dass Admins Benutzer manuell anlegen. Eine erfolgreiche SSO-Anmeldung überspringt die App-eigene TOTP-MFA-Abfrage, da der Identity Provider bereits eine eigene (typischerweise stärkere) Authentifizierung durchgesetzt hat. Der PKCE-code_verifierwird kurzlebig (5 min) in einem httpOnly-SameSite=Lax-Cookie zwischengespeichert statt serverseitig, damit der Flow ohne Sticky-Sessions über mehrere Backend-Replicas hinweg funktioniert; der Rückweg vom Callback zum Frontend nutzt ein kurzlebiges (60 s), zweckgebundenes JWT statt des echten Access-Tokens in der Redirect-URL.OIDC_ISSUER_URL/OIDC_CLIENT_ID/OIDC_CLIENT_SECRET/OIDC_REDIRECT_URIkommen ausschließlich aus Environment-Variablen; fehlen sie, meldetGET /api/v1/auth/sso/statusenabled: falseund die Login-Seite blendet den "Mit Microsoft anmelden"-Button aus, statt beim Start zu scheitern. -
M365-Kalender-Synchronisation als einseitiger, asynchroner Push: Abschnitt 4.25 verlangt nur einen internen Betriebskalender, keine externe Anbindung – diese Funktion ist eine reine Erweiterung ohne Lastenheft-Vorgabe. Umgesetzt als
CalendarSyncModule(ApiCore-Backend/src/calendar-sync/): App-only Graph-API-Zugriff per Client-Credentials-Flow (GraphClientService) statt delegierter Anmeldung, da die Synchronisation unbeaufsichtigt im Hintergrund laufen muss und nicht an eine interaktive Benutzersitzung gebunden sein darf. Nutzt bewusst dieselbe Entra-App-Registrierung wie das SSO-Login (OIDC_ISSUER_URL/OIDC_CLIENT_ID/OIDC_CLIENT_SECRET, ausschließlich Env-Variablen) statt einer zweiten, separaten App-Registrierung – ein Enterprise-App-Objekt im Tenant genügt, sofern es zusätzlich zu den delegierten Login-Scopes auch die Application-Permission „Calendars.ReadWrite“ (Admin-Consent) erhält; die Tenant-ID wird dafür aus dem Pfad vonOIDC_ISSUER_URLabgeleitet, statt eine weitere Konfigurationsvariable zu benötigen. Die Übertragung ist bewusst nur einseitig (ApiCore → M365-Gruppenkalender, adressiert übergroupId); Änderungen, die direkt im M365-Kalender vorgenommen werden, fließen nicht zurück. Statt eines synchronen Push im Request-Pfad (wie beim Webhooks-Modul) nutztCalendarSyncOutboxEntryein Outbox-Muster mit minütlichem@Cron-Worker: Graph-API-Aufrufe sind zu langsam/fehleranfällig (Token-Refresh, Drosselung, transiente 5xx), um eine Durchsicht-/Aufgaben-Erstellung darauf warten zu lassen; fehlgeschlagene Versuche werden bis zu 5-mal wiederholt, danach bleibt der EintragFAILEDbis zu einem manuellen "Jetzt synchronisieren" oder einer erneuten Änderung des Quelldatensatzes. Die Outbox ist sicher bei mehreren Replicas: Jeder Eintrag wird vor der Arbeit per bedingtem Update (claimedAt) reserviert, ein Claim älter als 5 min gilt als verwaist und wird übernommen, Graph-Requests haben ein 30-s-Timeout.revisionsorgt dafür, dass eine Änderung während des Syncs nicht verloren geht (Eintrag bleibt PENDING, dasselbe Event wird beim nächsten Lauf aktualisiert); ein Abbruch während des Syncs löscht das gerade geschriebene Event. Ein neues Graph-Event entsteht beim Aktualisieren nur, wenn das alte nicht mehr existiert (404/410); andere Fehler gelten als fehlgeschlagener Versuch. Jedes Anlegen schickt einetransactionId(aus Eintrag, Revision und ersetztem Event), damit Graph eine Wiederholung nach einem Timeout nicht doppelt anlegt; „Jetzt synchronisieren“ startet den Lauf im Hintergrund (202,runningim Status) und setzt FAILED-Einträge auf PENDING zurück.groupIdmuss eine Entra-Objekt-ID (GUID) sein;""odernulllöscht sie; sie wird in Graph-URLs kodiert. Fehler aus Prisma werden nur mit Name, Code und Modell geloggt (src/common/loggable-error.ts), nie mit Abfragewerten. Offene Entscheidung (2026-10-12): Laut Microsoft-Doku (group-post-events) unterstütztPOST /groups/{id}/eventskeine Application-Permission, nur delegiertesGroup.ReadWrite.All. Mit dem App-only-Token oben kann das Anlegen im Gruppenkalender deshalb in Produktion scheitern (nicht gegen einen echten Tenant geprüft). Auswege: Zielkalender auf ein Ressourcen- oder Benutzerpostfach umstellen (/users/{upn}/calendar/events, App-only erlaubt, per Application Access Policy begrenzt) oder delegierter Zugriff über ein Dienstkonto. Pro Quelltyp (Aufgabe/Durchsicht/Behandlung/Fütterung/Ernte/Königinnen-Zuordnung) ist die Synchronisation einzeln über die GUI (Verwaltung > Automatisierung > Kalender-Sync) abschaltbar; Kalendereinträge werden als ganztägige Graph-Events (isAllDay: true) angelegt, um Zeitzonen-/Uhrzeit-Mehrdeutigkeiten zu vermeiden, da die zugrundeliegenden Fachdaten selbst keine Uhrzeit führen. -
Wartezeit manuell statt aus Wirkstoff-Datenbank abgeleitet:
Treatment.withdrawalPeriodDayswird pro Behandlung von Hand eingetragen (laut Packungsbeilage des verwendeten Mittels), statt automatisch aus Produktname/Wirkstoff hergeleitet zu werden. Eine hartkodierte Zuordnung Produkt→Wartezeit wäre eine faktische, rechtlich relevante Behauptung über Tierarzneimittel-Zulassungen, die im Code weder aktuell gehalten noch verlässlich verifiziert werden kann; die Verantwortung für den korrekten Wert bleibt beim Imker. Die App warnt in der Ernte-Erfassung nur, wenn ein Wert hinterlegt wurde und dessen Fenster (endDate ?? date+withdrawalPeriodDays) noch nicht abgelaufen ist – bezogen auf das Erntedatum (GET /treatments/withdrawal-status?colonyIds=…&at=JJJJ-MM-TT, ohneatgilt jetzt; Behandlungen, die erst nach diesem Datum beginnen, zählen nicht) – sie blockiert die Ernte nicht hart, da Ernte-Datensätze auch nachträglich für bereits erfolgte, reale Ernten erfasst werden. -
Idempotenz per Opt-in: Anlege-Endpunkte, die Formulare und die Offline-Warteschlange wiederholen können (Durchsicht, Behandlung, Fütterung, Varroa, Schwarmkontrolle, Ernte, Aufgabe, Abfüllung, Lagerbewegung), tragen
@Idempotent(). Der Client sendet je Vorgang einenIdempotency-Key; eine Wiederholung liefert die gespeicherte erste Antwort (IdempotencyRecord, 90 Tage). Bewusst kein globaler Interceptor, da gespeicherte Antworten sonst Geheimnisse (MFA-Setup, Tokens) enthalten könnten. Die Tabelle ist flüchtig und nicht Teil des Backups. Idempotenz schützt nur, wenn nach dem Commit nichts mehr fehlschlägt: Darum laufen alle Seiteneffekte nach dem Commit (AutomationService.evaluate*,CalendarSyncService.enqueue/cancel,WebhooksService.dispatch) best-effort überrunAfterCommit(src/common/after-commit.ts) – sie loggen auf Error-Level und werfen nie. Was das Schreiben verhindern muss (z. B.generateNumber, Validierung), wirft weiter. Doppelte Automatik-Aufgaben verhindert eine Zeilensperre auf der Regel (SELECT … FOR UPDATE). Dauerhaft zugestellte Seiteneffekte bringt erst das Ereignis-Rückgrat (Specdocs/superpowers/specs/2026-10-12-plattform-rueckgrat-und-fahrplan-design.md). -
Automatische Aufgaben sind Vorschläge:
Task.sourceAutomationTriggerkennzeichnet von Regeln erzeugte Aufgaben. Je Regel und Objekt existiert höchstens eine offene Aufgabe. Keine voreingestellte Frist (fachlich nicht belastbar ableitbar); optionalconfig.dueInDaysje Regel. Die Varroa-Schwelle (Milben/Tag) wird nur mit Tagesraten des natürlichen Milbenfalls verglichen. Die Tagesrate wird bei jeder Korrektur einer Eingangsgröße (Methode, Anzahl, Dauer) neu berechnet; ein mitgesendeter, vom gespeicherten abweichender Wert gilt als manuell gesetzt und gewinnt. -
API-Schlüssel für Maschinen-Zugriffe: Integrationen (Wabenhain-Shop) nutzen einen Schlüssel
ack_…imAuthorization: Bearer-Header statt eines Service-Kontos mit Passwort. Gespeichert wird nur der SHA-256-Hash (32 Zufallsbytes, daher kein langsamer KDF nötig); der Klartext erscheint einmal bei der Erstellung. Ein Schlüssel ist kein Benutzer:JwtAuthGuardsetztrequest.apiKeystattrequest.user,PermissionsGuardlässt ihn nur auf Routen, deren Berechtigung in seinen Scopes steht –@AnyAuthenticatedUser-Routen nie. Erlaubte Scopes sind eine kurze Allowlist (api-keys/api-key-scopes.ts:shop-sync:read,shop-sync:write,erp-inventory:read); je angebundenem System ein eigener Schlüssel. Verwaltung mitapi-keys:manage. Schlüssel sind bewusst nicht Teil des Backups: nach einer Wiederherstellung wird ein neuer Schlüssel ausgestellt. -
Lebende vs. beendete Völker: Nur
DISSOLVED,SOLD,DECEASED,MERGEDsperren Umsetzen/Teilen/Vereinigen/Königinzuweisung (colonies/colony-status.ts).WEAK,CONSPICUOUS,QUEENLESSbleiben bearbeitbar. -
Bestandsbuch aus Behandlungen, nicht als eigenes Register: Das Bestandsbuch nach Art. 108 VO (EU) 2019/6 wird aus
Treatmenterzeugt (export/treatment-log.ts); Lieferant, Erwerbsbeleg und verschreibender Tierarzt sind Freitextfelder der Behandlung, Name/Anschrift des Halters liegen inSiteSettings. Der Stand am Behandlungstag wird beim Anlegen alsTreatment.apiaryIdfestgehalten (Umsetzen ändert ihn nicht nachträglich); für Altbestand wird er ausColonyApiaryHistoryabgeleitet und im PDF bei Unsicherheit markiert. Fehlende Pflichtangaben werden als „FEHLT“ gedruckt statt still weggelassen. Keine Wartezeit-Automatik (siehe 10). -
Überwinterung als eigene Einträge statt Volksstatus:
WinterCheck(eindeutig je Volk,winterYear, Saison) hält Ein- und Auswinterung; ab Juli gehört ein Datum zum Winter desselben Jahres. Verlustursachen sind bewusst nur die COLOSS-Kategorien, keine Diagnosen. Die Übersicht gruppiert nach dem aktuellen Stand des Volkes. Ein Verlust ändert den Volksstatus nur auf ausdrücklichen Wunsch (markColonyDeceased). Neue Rechte vergibt die Migration an Nicht-System-Rollen, weil der Katalog-Sync nur System-Rollen berechtigt. -
Schutz gegen Rechteausweitung in der Benutzerverwaltung: Wer Benutzer pflegt, darf nur Konten verwalten (Passwort/MFA zurücksetzen, E-Mail ändern, (de)aktivieren) und Rollen vergeben, deren Rechte er selbst vollständig hat (
UsersService.assertWithinCallerPermissions), analog zur Rollenbearbeitung. -
SSO-Bindung an die IdP-Identität: Die erste Microsoft-Anmeldung bindet das Konto an
"<iss>|<oid>"(User.ssoSubject); danach zählt nur noch diese Identität, nicht der unverifizierte E-Mail-Claim. Mandantenübergreifende Issuer (common/organizations/consumers) schalten SSO ab. E-Mails werden ohne Groß-/Kleinschreibung verglichen. -
Backups versiegelt: AES-256-GCM über das gzip-Bundle, Schlüssel
BACKUP_ENCRYPTION_KEY(sonst per HKDF ausJWT_ACCESS_SECRET). Nur versiegelte Dateien stellen vollständig wieder her (und überschreiben). Eine unversiegelte (alte v2-)Datei darf nur die 27 Stamm- und Feldtabellen der Erlaubt-ListeLEGACY_UNSEALED_TABLESergänzen – fehlende Zeilen anlegen, bestehende nie ändern. Alles andere (Benutzer, Rollen, Webhooks, Audit-Log, Kalender-Sync, Backup- und Website-Einstellungen, Nummernkreise, Bestands- und Chargenbewegungen, Shop-Anfragen, Rohstoffe/Herstellung, Dokumente, Benachrichtigungen und jede künftige Tabelle) kommt nur aus versiegelten Dateien; die Oberfläche listet übersprungene Tabellen auf; Import verlangt Passwort (+ TOTP). Webhook-Secrets liegen verschlüsselt in der DB (Schlüssel ausJWT_ACCESS_SECRET) – für eine Wiederherstellung in einem anderen Deployment mit anderemJWT_ACCESS_SECRETmüssen Webhook-Secrets und MFA neu eingerichtet werden. -
Bearbeiten-Regel für
null(Grundsatz „alles bearbeitbar“): In jeder Bearbeiten-Anfrage bleibt ein weggelassenes Feld unverändert,nullleert ein Feld, das leer sein darf, undnullauf einem Pflichtfeld wird mit 400 abgelehnt (statt still ignoriert oder als 500). Umgesetzt überPartialType(..., { skipNullProperties: false })in den Update-DTOs,@IsOptionalNotNull()(src/common/validation/is-optional-not-null.ts) für weglassbare, aber nicht leerbare Felder undoptionalDate()(src/common/patch-values.ts) für leerbare Datumsfelder;update-dto-null-handling.spec.tshält den Vertrag je DTO fest. Das Frontend sendet geleerte optionale Felder überemptyToNull(ApiCore-Frontend/src/lib/forms/empty-to-null.ts) alsnull. -
Lebende Völker: „Lebend“ heißt jeder Status außer
DISSOLVED,SOLD,DECEASED,MERGED– also auchWEAK,CONSPICUOUS,QUEENLESS. Backend-Abfragen filtern überLIVING_COLONY_WHERE(colonies/colony-status.ts), das Frontend überisLivingColony(lib/colonies/status.ts);status = ACTIVEsteht nur dort, wo wirklich genau dieser Status gemeint ist.
7. Kubernetes
deploy/k8s/ enthält je Projekt: deployment.yaml, service.yaml, ingress.yaml, sowie configmap.example.yaml und secret.example.yaml (Platzhalter, keine echten Werte). Backend exponiert GET /health (liveness) und GET /health/ready (readiness, prüft DB-Verbindung). Beide Projekte je ein schlankes Multi-Stage-Dockerfile (Build-Stage mit vollem node_modules, Runtime-Stage mit npm ci --omit=dev bzw. Next.js output: 'standalone').
deploy/k8s/postgres/ enthält das PostgreSQL-StatefulSet (ein Replica, volumeClaimTemplates für persistente Daten, postgres:18-alpine), den zugehörigen headless Service (apicore-postgres, DNS-Name fürs Backend) sowie secret.example.yaml für POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB. deploy/k8s/backend/secret.example.yamls DATABASE_URL zeigt auf diesen Service.
8. Backup & Restore
Schema-Migrationen (Reproduzierbarkeit): Ausschließlich prisma/migrations/ als Quelle der Wahrheit. Lokal/Entwicklung: npx prisma migrate dev. Gegen jede Produktions- oder Staging-Datenbank ausschließlich npx prisma migrate deploy (idempotent, wendet nur fehlende Migrationen an, kein Schema-Drift). prisma db push wird an keiner Stelle im Code, in Skripten oder im Dockerfile verwendet und ist für Produktions-/Staging-Umgebungen untersagt. Der Backend-Container führt migrate deploy und danach npm run seed selbst beim Start aus, bevor der Server Verbindungen annimmt (Dockerfile-CMD: npm run migrate:deploy && npm run seed && node dist/main.js) – kein separater Kubernetes-Job. Bei replicas: 2 starten mehrere Pods potenziell gleichzeitig; das ist unproblematisch, da Prisma für migrate deploy einen datenbankseitigen Advisory-Lock nutzt und prisma/seed.ts per Unique-Constraint auf User.email idempotent ist (verlierender Pod bekommt einen P2002-Fehler, den seed.ts abfängt und als No-op behandelt statt den Container abstürzen zu lassen) – eine Instanz wendet die Migrationen/den Seed an, die anderen finden danach nichts mehr zu tun. deploy/k8s/backend/deployment.yaml hat dafür eine startupProbe mit großzügigem failureThreshold, damit liveness/readiness den Container nicht neu starten, während die (beim allerersten Deploy ggf. länger laufende) Migration noch durchläuft.
Erster Admin-Zugang: prisma/seed.ts legt beim allerersten Start (leere User-Tabelle) einen Administrator an (SEED_ADMIN_EMAIL/SEED_ADMIN_PASSWORD, Default [email protected]/ChangeMe123!) und gibt URL (FRONTEND_ORIGIN), Benutzername und Passwort einmalig im Container-Log aus. Existiert der User schon (auch deaktiviert), passiert nichts – kein erneutes Log, kein erneutes Anlegen, damit spätere Passwortänderungen nicht durch veraltete Logs verwirrend überschrieben wirken und Zugangsdaten nicht bei jedem Neustart in die Logs wandern. Ersetzen: neuen Admin über /settings/users anlegen, dann den alten deaktivieren (Löschen gibt es nicht, siehe "Historie statt Löschen") – die Lockout-Sperre in UsersService.assertNotLastSystemRoleUser verhindert ohnehin, den letzten aktiven Admin zu deaktivieren.
Datensicherung: Seit der Entscheidung, PostgreSQL selbst im Cluster zu betreiben (siehe CLAUDE.md), liegt die Backup-Verantwortung bei diesem Projekt, nicht mehr bei einem externen Cluster-/DBA-Betrieb. Umgesetzt als anwendungsseitiges BackupModule (ApiCore-Backend/src/backup/) statt der ursprünglich hier vorgesehenen reinen pg_dump/PITR-Strategie:
- Export/Import:
BackupService.exportAll()/.importAll()sichert alle fachlichen und Konfigurationsmodelle (Betriebsdaten inkl. Herstellung, Rohstoffe, Bewegungsbuch und Shop-Anfragen, Nummernkreise, Benutzer/Rollen/Rechte, Audit-Log, Website-/Backup-/Kalender-Einstellungen samt Kalender-Outbox, hochgeladene Dokumente inkl. Dateiinhalt als Base64) als eine versionierte JSON-Datei (Bundle-Version 3; ältere Bundles bleiben einspielbar). Bewusst ausgenommen sind nur Permission (Katalog aus dem Code), RefreshToken, ConsumedToken, ApiKey (Annahme 14) und IdempotencyRecord (Annahme 11); die Liste mit Begründung steht inMODELS_EXCLUDED_FROM_BACKUP, ein Test erzwingt sie für jedes neue Prisma-Modell. Import ist upsert-basiert (Rollen/Berechtigungen überkey/resource+actionstatt roherid, damit ein Restore auch in ein anderes Deployment funktioniert) und läuft in einer einzigen$transaction. - Automatischer Zeitplan:
BackupSchedulerServiceregistriert einen@nestjs/schedule-Cron-Job, der die Bundle-Datei periodisch aufBACKUP_STORAGE_PATHschreibt und alte Stände nachBACKUP_RETENTION_COUNTrotiert. Pfad/Zeitplan/Aufbewahrung sind sowohl per Env-Variable als auch über die GUI (Verwaltung > Backup) konfigurierbar – eine gesetzte Env-Variable hat dabei immer Vorrang vor dem GUI-Wert. Beireplicas: 2verhindert ein Datei-Mtime-Guard (kein DB-Lock nötig), dass beide Pods im selben Tick doppelt sichern; ohne gesetzten Pfad bleibt der Zeitplan inaktiv, manueller Export/Import über die UI funktioniert davon unabhängig immer. - Restore: Über
POST /backup/import(bzw. den Import-Dialog unter Verwaltung > Backup) eine zuvor exportierte Bundle-Datei einspielen. Import-LimitBACKUP_MAX_IMPORT_BYTES(Standard 50 MB, höchstens 256 MB, weil der Upload vollständig im RAM des 512-Mi-Pods verarbeitet wird); Export und Zeitplan prüfen jede Sicherung dagegen und warnen in Log und UI (Verwaltung > Backup), bevor eine Datei entsteht, die sich im Notfall nicht mehr einspielen ließe.
Ein rohes pg_dump/PITR-Backup auf Infrastrukturebene bleibt als zusätzliche, komplementäre Absicherung für den Katastrophenfall (z. B. korrupte Datenbankdateien, nicht nur fachliche Daten) sinnvoll, ist aber nicht mehr der primäre, hier dokumentierte Mechanismus – die in-App-Lösung deckt den fachlichen Anwendungsfall "Daten sichern/übertragen" bereits vollständig ab.
9. API-Sicherheit (Stand Oktober 2026)
Grundlage: OWASP API Security Top 10 (2023) und OWASP ASVS Level 2. Umsetzung und Begründung je Punkt in docs/pruefung-2026-09.md, Abschnitt „API-Sicherheit“.
| Bereich | Maßnahme |
|---|---|
| Authentifizierung | JWT mit festem Algorithmus, Issuer und Audience je Zweck; MFA (TOTP) mit Replay-Schutz; Kontosperre 15 min nach 5 Fehlversuchen; gleiche Antwort für unbekannte/gesperrte Konten; bcrypt auf jedem Pfad |
| Passwörter | mind. 12 Zeichen, max. 72 Byte, Abgleich mit Have I Been Pwned (k-Anonymität, abschaltbar per PASSWORD_BREACH_CHECK=false) |
| Sitzungen | Refresh-Rotation atomar, Diebstahlerkennung, max. 7 Tage; Einmal-Tokens für SSO-Abschluss und MFA-Schritt |
| Step-up | Backup-Import, MFA ein/aus verlangen aktuelles Passwort (+ TOTP) |
| Autorisierung | fail-closed PermissionsGuard; keine Rechteausweitung über Benutzer-/Rollenpflege; Suche nur über lesbare Objektarten |
| Maschinen-Zugriff | API-Schlüssel nur als Hash (Suche über den Hash, kein Vergleich von Geheimnissen), Scope-Allowlist (shop-sync:read, shop-sync:write, erp-inventory:read), IP-Allowlist, widerrufbar, befristbar; Ersteller kann nur eigene Rechte vergeben; nach 20 ungültigen Schlüsseln in 15 min je IP (IPv6: je /64) 429 für weitere ungültige – gültige Schlüssel nie, falsch aufgebaute Tokens zählen nicht; Warnung beim Start, wenn TRUST_PROXY in Produktion fehlt; Logs nur mit Schlüssel-Präfix |
| Ausgehende Aufrufe | Webhooks: Ziel-IP beim Verbindungsaufbau geprüft (kein DNS-Rebinding), nur Port 80/443, https in Produktion, 5 s Timeout, Signatur über Zeitstempel+ID+Body |
| Transport/Antworten | Cache-Control: no-store (außer öffentliche Chargen-Seite), CSP default-src 'none', frame-ancestors 'none', keine ETags; Produktion startet nur mit https-FRONTEND_ORIGIN und sicheren Cookies |
| Rate-Limits | Throttler vor der Authentifizierung (100/min, Login/MFA/Refresh/Logout 10/min, öffentliche Chargen 30/min); Client-IP über TRUST_PROXY |
| Daten | Backups verschlüsselt und signiert, Dateien 0600; CSV-Export ohne Formel-Injection; Logs ohne Query-Strings |
Bekannte Restpunkte (bewusst nicht umgesetzt):
- Rate-Limits zählen je Pod im Speicher; bei 2 Replikas verdoppelt sich das Limit. Brute-Force-Schutz hängt nicht daran, sondern an der Kontosperre in der DB. Ein gemeinsamer Speicher (Redis) wäre der nächste Schritt, falls Lastspitzen ein Thema werden.
- Eine E-Mail-Änderung im eigenen Profil meldet „bereits vergeben“, wenn die Adresse existiert. Das erfordert eine Anmeldung plus aktuelles Passwort; eine generische Meldung würde Nutzer nur verwirren.