Files
HiveGrid-Wireframes/hivegrid-cloud/docs/ARCHITECTURE-DECISIONS.md
T

32 KiB
Raw Blame History

HiveGrid 2.0 — Architektur-Entscheidungen

Entscheidungen aus dem Boilerplate-Abgleich (Stand 2026-06-04). Diese ADR-Liste ergänzt AGENTS.md und ist verbindlich für die Umsetzung. Grundlage: docs/boilerplate-mapping/_SUMMARY.md + die Modul-Reports. Legende: ✅ entschieden · 🅿️ Backlog (Entscheidung später).


AD-1 — Mandanten-Trennung: echtes Postgres-RLS ✅

Entscheidung: Row-Level-Security in Postgres als bauliche Vorgabe (nicht nur App-Level-Scoping).

  • RLS-Policies (USING/WITH CHECK) als Forward-Migration mit Inverse-SQL-Kommentar
  • Request-Middleware setzt SET LOCAL app.tenant_id
  • App-DB-Rolle ohne BYPASSRLS
  • Worker-/Job-Tenant-Context-Wrapper
  • Cache-Präfix tenant:{id}:, S3-Pfad tenants/{id}/
  • Ausnahmen ohne RLS: users, subscription_*, feature_flags, Branchen-Templates

Warum: Spec A.4 + M019-Erfolgskriterium (Pen-Test). Defense-in-depth — ein vergessener Query-Filter kann keine Fremddaten mehr leaken. Boilerplate hat nur App-Level-Scoping. Betrifft: M019 (+ jedes tenant-scoped Modul).

AD-2 — Daten-Modell: typisierte Tabellen, Entity-Builder raus ✅

Entscheidung: HiveGrid-Fachentitäten (CRM, Deals, Aufträge, Tasks, …) als dedizierte, typisierte First-Class-Prisma-Tabellen. Das Entity-Builder-CRM der Boilerplate (Entity+Row+RowValue, apps/web/app/modules/crm) wird entfernt, nicht angepasst. Custom-Felder über purpose-built CustomFieldDefinition + CustomFieldValue (JSONB-Wert), nicht über die polymorphe Row-Engine.

Warum: Spec M01 verlangt typisierte Tabellen mit Constraints, Indizes (pg_trgm, tenant_id, archived_at, last_activity_at), ContactEmail/ContactPhone, M:N-Relationen, zentrale Activity-Tabelle. Polymorphes RowValue ist damit unvereinbar (Performance bei 10k+, Indizes, Permissions, RLS). Zwei Architekturen parallel = Fremd-Architektur sickert ein. Betrifft: M01, M02, M03, M04, M26 (+ alle Fachmodule).

🔄 REVIDIERT 2026-06-13 (Modell-Pivot, siehe AD-20): Deal + Project verschmelzen zu EINER typisierten Projekt-Tabelle (Superset der Felder) + CustomFieldDefinition/Value + einem neuen Pipeline-Feld-Config-Layer (welche Felder pro Pipeline+Status sichtbar/pflichtig sind; nicht sichtbare Felder bleiben im Hintergrund erhalten). Die Konfigurierbarkeit ist Metadaten über typisierten Spalten — der generische Entity-Builder (Entity/Row/RowValue) bleibt entfernt. AD-2-Prinzip („typisiert, kein Generikum") bleibt gewahrt. Anker: .planning/ENTITY-MODEL.md.

AD-3 — Plattform-Admin-Identität: getrennt, Email+PW+TOTP, optional Authentik-SSO ✅

Entscheidung: Vollständig getrennte Admin-Welt — eigene AdminUser-Tabelle (eigenes passwordHash/totpSecret/status), eigene AdminSession (server-side, IP-gebunden), eigene AdminRole/AdminPermission-RBAC. Primärer Login: Email + Passwort + TOTP (Magic-Link verboten). Zusätzlich optionaler Authentik-OIDC/SSO-Login, gemappt auf dieselbe getrennte Admin-Identität. Kein Generalschlüssel zwischen Plattform und Mandanten.

  • AdminAuditLog braucht kein neues Schema: vorhandener AuditEvent (Hash-Kette, tenantId=null = plattformweit) deckt das ab.
  • Boilerplate-Admin-Auth (Login-Flow, MFA-Step-up-Gate, Rate-Limit, Session-Cookie-Policy) dient als Vorlage, nicht 1:1 (BP hat AdminUser nur als User-Extension).

Warum: Spec 37.1 / M034 verbietet den geteilten Identity-/RBAC-Pfad explizit. Betrifft: M034–M041 (M37).

AD-4 — Sessions: Postgres als Source-of-Truth + Redis-Cache ✅

Entscheidung: Server-side Session-Tabelle in Postgres (Geräte-Liste mit Browser/IP/ Standort/letzter Aktivität, Revoke, „von allen Geräten abmelden", 4h-Inaktivität, 30-Tage-Sliding-Window, Auto-Logout bei Passwort-/2FA-/Email-Änderung) als Quelle der Wahrheit; Redis-Cache für schnelle Pro-Request-Validierung. Cookie hält nur die Session-ID-Referenz.

Warum: Spec B.5 braucht dauerhafte, abfragbare Geräte-Liste + verlässliches Revoke-all; reine Cookies (BP-Default) können nicht serverseitig widerrufen werden; reines Redis verliert Geräte-Historie bei Flush. Betrifft: M021 (Mandanten-User), M034 (AdminSession), M141 (Customer-Portal).

AD-5 — Such-Engine: Meilisearch (self-hosted) + pgvector-Hybrid ✅

Entscheidung: Keyword-Suche über das selbst gehostete Meilisearch (BP-@toolctrl/search weitgehend 1:1), semantische Suche über pgvector + EU-Embeddings (Scaleway 1024-dim, BP 1:1), kombiniert via RRF-Fusion. Embedding-Pipeline + Outbox + Indexer-Worker aus der BP übernehmen.

Warum: Meilisearch wird ohnehin selbst gehostet (EU-Datenhoheit + Ops bereits gelöst) → bessere Tippfehler-Toleranz/Relevanz + maximaler Reuse. Weicht bewusst von M056 „Postgres-FTS, kein Extra-Service" ab → Change-Request gegen M056 nötig. Betrifft: M24 / M056–M058.

AD-6 — EU-Email-Provider ✅

Entscheidung (2026-06-16, EXT-02): Mailjet (EU/FR) als EU-Email-Provider für transaktionale und Marketing-Mails. MailerSend (BP-Default) wird verworfen — MailerSend, Inc. ist US-Entität (Transfers via EU-US Data Privacy Framework), unter strikter LP-13-Lesart („keine US-Gesellschaft im Boot", wie bei OpenAI/Anthropic) problematisch.

Warum: EU-Datenhoheit ohne US-Gesellschaft im Boot; feature-reich (transaktional + Marketing

  • A/B in P14). Boilerplate ist bereits auf MAILJET_* verdrahtet.

Verifikationsstand (EXT-02): Sende-Domain hivegrid.de bei Mailjet verifiziert — SPF (include:spf.mailjet.com) + DKIM (Selector mailjet) Auto-Probe grün, Sender *@hivegrid.de aktiv; DMARC p=none (Monitoring während Warm-up). Cleanup-Follow-ups: (a) include:_spf.mailersend.net aus dem SPF-Record entfernen, sobald die Migration vollständig ist; (b) DMARC nach Warm-up von p=none auf p=quarantine ziehen.

Betrifft: M09, M10, M22, M102.

AD-7 — Captcha / Bot-Schutz 🅿️ (Backlog)

Status: Entscheidung später. Tendenz: EU — Friendly Captcha (DE, GDPR-first) oder self-hosted mCaptcha (passt zum Meilisearch-Self-Host-Ansatz).

  • Turnstile (BP) und hCaptcha (Spec) sind beide US-Firmen → bei strikter Linie Change-Request. Betrifft: Login (M020), Public-Forms (M05/M125), Booking (M06).

AD-8 — Verschlüsselung: Per-Tenant-DEK + Envelope ✅

Entscheidung: Pro Mandant ein Data-Encryption-Key (DEK), verschlüsselt mit einem Master-Key (Scaleway Key Manager o.ä.) — Envelope-Encryption. Crypto-Shredding bei Mandanten-Löschung (M165: Tenant-Key wegwerfen → alle verschlüsselten Daten unlesbar).

Warum: Spec verlangt Per-Tenant für Passwort-Safe (M134) + OAuth-Tokens; globaler Key (BP-Default) exponiert bei Kompromiss alle Mandanten und erlaubt kein Crypto-Shredding. Betrifft: M134 (HR-Passwort-Safe), OAuth-Tokens (M09/M25/M12), M165 (DSGVO-Löschung).

AD-9 — Realtime: WebSocket für Chat + SSE für Read-Updates ✅

Entscheidung: Generischer WebSocket-Room-Layer für bidirektionale Echtzeit (Team-Chat M13: Rooms, Presence, Typing, Read-Receipts). SSE (BP-Default) bleibt für einseitige Live-Updates (Kanban M03, Notifications M22). hocuspocus bleibt CRDT-only (Kollab-Dokumente).

Warum: SSE ist einseitig → für Chat ungeeignet; hocuspocus ist CRDT, kein Chat-Broadcast. Betrifft: M13 (M110–M114); SSE-Pfad: M03, M22.

AD-10 — Naming-Konvention: @toolctrl/* behalten, Docs anpassen ✅

Entscheidung: Die geteilten Boilerplate-Pakete behalten ihre Namen (@toolctrl/*, packages/ui-app) — kein flächiger Rename zu @hivegrid/*/ui-hivegrid.

  • @toolctrl/* + ui-app/ui-subframe/ui-mock = geteilte Boilerplate-Pakete (kommen von upstream, bleiben mergebar).
  • @hivegrid/* = nur für genuin neue HiveGrid-Pakete, die upstream nie existieren (z.B. das neue Activity-Paket, HiveGrid-Domänen-Pakete) → keine Merge-Konflikte.
  • HiveGrid-Doku (AGENTS.md, docs/INTEGRATION-CONTRACTS.md, PLANs, Reports) wird auf diese Realität gezogen; wo Docs ui-hivegrid/@hivegrid/foundation sagen, ist ui-app/ @toolctrl/foundation gemeint.

Warum: Es sind 901 Dateien / 1649 @toolctrl-Vorkommen. Ein Rename würde jeden künftigen git merge upstream/main (AD-Setup: Git-Downstream) massiv kollidieren lassen und die Update-Fähigkeit zerstören. ToolCTRL ist die Firma, HiveGrid das Produkt — geteilte Pakete unter @toolctrl sind korrekt. Betrifft: Querschnitt (alle Module), Phase 0.


Delivery- & Scope-Entscheidungen

AD-11 — Bau-Strategie: Hybrid (Fundament-Kern, dann Geerkens-Kernpfad) ✅

Entscheidung: Nach Phase 0 zuerst der Fundament-Kern (M27 Auth + M33a Apps-Engine + M01 CRM-Datenmodell), dann sofort die Geerkens-Wertschöpfungskette Lead → Projekt (konfigurierbare Pipelines) → KWP, danach die Breite (HR, Portal, Marketing, Mobile, Billing-Feinheiten). Balance aus sauberem Fundament und früh sichtbarem Nutzen.

🔄 REVIDIERT 2026-06-13 (Modell-Pivot, AD-20): Kette ist nicht mehr „Lead → Sales(Deal) → Auftrag(Project)", sondern Lead → Projekt → KWP. Sales und Auftrag sind konfigurierbare Pipeline-Views auf denselben Projekt-Kern.

AD-12 — SHK-Domänen-Defaults: aus Spec als Seed, Noah validiert ✅

Entscheidung: Die SHK-Inhalte (10 Standard-Rollen + Permission-Bundles [27.7/N1], SHK-Sales-Pipeline-Stufen [M59], Standard-Custom-Felder + Tags [M44], Provisionierungs-Daten [M158]) werden aus Spec/Pflichtenheft als deklaratives, leicht änderbares Seed umgesetzt; Noah/Geerkens validieren iterativ im Klick-Pfad. Blockiert den Bau nicht.

🔄 AMENDMENT 2026-06-19 (Owner-Entscheidung Raffael, Partner-validiert): Die spekulativen „10 Standard-Rollen" aus 27.7/N1 sind auf die 6 vom Partner gelieferten Rollen festgelegt: Geschäftsführer, Projektleiter, Verwaltung, Vertriebler, Kundensupporter, Monteur (Seed apps/web/app/utils/services/shkRoles.server.ts, idempotent, is_demo_data). Das deklarative-Seed-Prinzip bleibt unverändert; nur die Anzahl/Liste ist fixiert. R-P2b-DoD + Planungs-Docs (.planning/, Milestone-Layer) entsprechend von 10 auf 6 nachgezogen.

AD-13 — Go-Live-Scope: breiter Launch ✅

Entscheidung: Der erste Geerkens-Go-Live umfasst nicht nur die Vertriebskette, sondern auch HR-Hub, Customer-Portal, Marketing und Mobile. Scope ≈ Großteil der 35 Module. (Sequenzierung trotzdem nach AD-11.)

AD-14 — Migration: sauberer Start + KWP-Sync füllt nach ✅

Entscheidung: Geerkens startet weitgehend leer; KWP-Sync (M089ff) bringt Auftrags-/Stammdaten automatisch. HubSpot/Quiply-Altdaten bleiben extern als Read-only-Archiv; nur punktuelle CSV-Importe via packages/imports. Keine vollen Migrations-Pipelines pro Quelle zum Launch.

AD-15 — Pricing/Billing: Engine bauen, Preise geparkt ✅

Entscheidung: Stripe-Integration + Seat-/Plan-Engine + Verbrauchs-Metering (M153–M156) werden technisch gebaut, die konkreten Preis-Stufen (Cloud/Cloud+/Enterprise, Seat-Preise, Trigger-Volumen, Setup-Fee) bleiben geparkt. Geerkens läuft auf manueller Sonderkondition/Trial. Self-Service-Checkout-Preise = eigene spätere Pricing-Runde.

AD-16 — USP-Säulen (Automation + KI): volle Tiefe zum Launch ✅ (⚠ KI-Hälfte abgelöst von AD-26, 2026-06-22)

Entscheidung: Kompletter visueller Automation-Editor (M075–M082, alle Trigger/Aktionen, 24h-Undo, Guards) und voller KI-Layer (M083–M088, EU-Provider, RAG, Sprach-zu-Automation, Pay-as-you-go) sind day-1 dabei — nicht nur Fundament.

⚠ KI-Hälfte abgelöst durch [AD-26] (2026-06-22): Kein selbst-gehostetes LLM/RAG/HiveMind mehr — KI kommt via MCP-Server (externe Clients), M083–M088 gestrichen. Die Automation-Säule bleibt unverändert day-1.

AD-17 — Mobile: PWA-only (v1 + v2), kein nativer Client ✅ (revidiert 16.06.)

Entscheidung (revidiert 2026-06-16): Mobile läuft ausschließlich als installierbare PWA / responsive Web — in v1 und v2. Monteur kann auf der Baustelle offline Aufträge bearbeiten, Abnahmen erfassen, Plantafel sehen; alles wird über Service-Worker + IndexedDB-Offline-Queue + Background-Sync gequeued und bei Sync mit „KWP gewinnt" aufgelöst (KWP-Sync = v2). Kein Expo, kein Capacitor, keine native iOS/Android-App, keine App-Store-Einreichung. Push = Web-Push (VAPID) — aktiviert in v2 den in P1 ruhenden notify()-Push-Kanal. Native-App + App-Store → frühestens v3 (Backlog, nicht beauftragt).

Historie: 10.05. voller Offline-Betrieb (Capacitor, M167–M171) · 11.06. Capacitor→Expo + native Hülle nach v2 · 16.06. komplett auf PWA-only reduziert (kein nativer Client in v1/v2; M167–M171 entfallen, „Mobile-App"-Arbeit = PWA-Ausbau: Installierbarkeit, Service-Worker, Offline-Härtung, Web-Push).

AD-18 — Customer-Portal: voll inkl. Skalierungs-Sicht ✅

Entscheidung: Komplettes Portal day-1: Projekt-Sicht + Upload + Reklamation + Subunternehmer-Zugang + Skalierungs-Sicht (Service-Inbox + Routing + Pro-Kunden-Lebensdauer), M141–M146 inkl. [N19].

Scope-Muster (historisch): AD-13/16 wählen den maximalen Umfang → der erste Go-Live ist faktisch das vollständige Phase-1-Produkt in großer Tiefe. Sequenzierung nach AD-11. AD-17 (Mobile) und AD-18 (Portal) wurden später revidiert (AD-18 → v2; AD-17 → PWA-only v1/v2) und folgen diesem Maximal-Muster nicht mehr. Dies bleibt eine bewusst große Scope-Wahl (siehe Scope-Check in der Session + SCOPE-V1-CUTS.md).

AD-19 — Go-Live-Modus: Big-Bang ✅

Entscheidung: Geerkens geht erst live, wenn das vollständige Phase-1-Produkt steht — ein großer Launch, kein phasiertes Produktiv-Go-Live. Interne Bau-Reihenfolge bleibt AD-11 (Fundament → Kernpfad → Breite), aber es gibt kein inkrementelles Produktiv-Schalten einzelner Module für Geerkens.

Bewusst akzeptiert: längste Zeit bis zum ersten echten Nutzer-Feedback, höchstes „am-Alltag-vorbei"-Risiko. Gegenmaßnahme empfohlen: Noah/Geerkens-Validierung pro Modul über den PROGRESS-TRACKER-Klick-Pfad (READY-TO-VERIFY → Sign-Off) als Feedback-Ersatz vor dem Big-Bang.

AD-20 — Projekt-zentrisches CRM/Pipeline-Modell ✅ (NEU 2026-06-13)

Entscheidung: Das CRM/Pipeline-Modell wird Projekt-zentrisch (auf Basis von Davids UX-Briefing, voll adoptiert). Statt getrennter Deal- und Project-Entitäten gibt es EINEN Projekt-Kern, auf den frei konfigurierbare Pipelines als Views schauen. „Deal" und „Auftrag" sind nur Pipeline-Namen/-Typen, keine eigenen Objektarten.

  • Pipeline = View: Admin legt Pipelines an/benennt sie, steuert Zugriff (wer sieht), Feld-Sichtbarkeit, Pflichtfelder pro Pipeline/Status und die Statuswerte. Ein Projekt ist genau einer Pipeline zugeordnet; Status ist pipeline-intern (kein globaler Projektstatus). Nicht sichtbare Felder bleiben im Hintergrund erhalten.
  • Lead-Flow: Lead/LeadInput ist Vorstufe mit Datenpunkt Weiterverarbeitungsfreigabe (Ja/Nein/Unklar); bei Freigabe → Vererbung ins Projekt. Lead → Projekt (das frühere Lead→Contact-Promotions-Modal entfällt; Kontakt wird separat verknüpft).
  • Kontaktmodell: Kontakt ist Oberbegriff mit Kontakttyp (Privatperson/Organisation), Kontaktstatus (Kontakt/Kunde/Bestandskunde) und Kontaktart (Haupt-/Sekundärkontakt als Rolle/Relation).
  • Scope-Calls (13.06.): Abnahme bleibt v1 als Checklisten/Protokoll-Sub-Area am Projekt (Mängel = Checkliste); Material/Zeiterfassung/Leistungspositionen/Baustelle/Support → v2-Backlog. Rechnung = einfaches Sub-Entity am Projekt in v1. KWP-Felder bleiben v2-Sync, v1 nur Feld-Halter.

Warum: Davids konsolidiertes UX-Modell wird zur Bau-Grundlage gemacht; Raffael richtet die Planung daran aus (statt David einzunormen). Höhere Konfigurierbarkeit (SHK-Betriebe brauchen frei benennbare Pipelines) bei erhaltenem typisiertem Kern. Betrifft: AD-2 (revidiert), AD-11 (Kette revidiert), M01–M04, ENTITY-MODEL, ROADMAP P6–P9, REQUIREMENTS, SCREEN-MAP. Anker: .planning/ENTITY-MODEL.md.

AD-21 — Prisma: Schema-Folder + Migrations-Disziplin für parallele Branches ✅ (NEU 2026-06-16)

Kontext: 2 Devs (Raffael + Akeshan) bauen Modul-Phasen parallel in eigenen Branches. Risiko: Kollisionen im Datenmodell und in der Migrations-Historie.

Befund (16.06.): Prisma 7.8.0; das Schema ist bereits als Ordner organisiert (prisma/schema/ mit 29 Domänen-Dateien: core, accounts, permissions, appsEngine, email, workflows, notifications, marketingLeads, ai …). Die Datei-Ebene ist damit gelöst — Module in verschiedenen Domänen-Dateien kollidieren nicht. Es existieren bereits 153 Migrations (lineare Timestamp-Historie). Connection-URLs in prisma.config.ts (Prisma 7): DIRECT_URL für Migrate, DATABASE_URL via PgBouncer zur Laufzeit.

Verbleibende Risiken: (1) core.prisma/accounts.prisma sind der geteilte Kern (Tenant/User/Audit/File/ApiKey/Credential) — gleichzeitige Relations-Edits kollidieren in dieser Datei. (2) Zwei Branches mit je migrate dev erzeugen divergierende Migrations-Ordner → „applied out of order" beim Merge.

Entscheidung — drei verbindliche Konventionen:

  1. Ein Modul = eine eigene prisma/schema/<modul>.prisma. Neue Modelle nie in core.prisma/accounts.prisma. Diese nur für Relationen zu Tenant/User anfassen, Rück-Relationsfelder dort minimal halten.
  2. Migrationen erst beim Merge. Auf dem Feature-Branch mit prisma db push iterieren (kein Migrations-File). Die eine Migration pro Phase erzeugt der Dev, der zuerst auf main merged — Forward-only mit Inverse-SQL als Kommentar (AGENTS.md DoD 28). Tägliche Integration hält Diffs klein.
  3. CI-Gate vor Merge: prisma validate + prisma migrate status grün (pre-push-Hook schärfen).

Warum: Schema-Splittung erledigt die Datei-Konflikte bereits; was bleibt (core-Hotspot + lineare Migrations-Historie) löst Konvention/Disziplin, nicht neue Technik. Betrifft: alle parallel gebauten Modul-Phasen (P2–P21, v2), AGENTS.md §Prisma-Schema, lefthook pre-push. Anker: prisma/schema/, AGENTS.md.

AD-22 — GeoIP lokal + IP-Anonymisierung (EXT-12) ✅

Entscheidung: Geo-Auflösung von Login-/Session-IPs über eine lokale GeoIP-Datenbank (DB-IP Lite Country, CC-BY-4.0, MMDB-Format) statt eines externen Dienstes. Die gespeicherte IP wird pseudonymisiert (HMAC-SHA256 mit geheimem Pepper); persistiert wird nur das Land (countryCode/countryName). Kein EU-SaaS-Geo-Dienst, kein ipstack (US/apilayer, LP-13).

Warum:

  • Ein externer Geo-Dienst — auch ein EU-gehosteter — bekäme die Endnutzer-IP (personenbezogen) übermittelt → DPA + Subprozessor (M166) + Latenz + Abhängigkeit. Der Transfer selbst ist das Risiko.
  • Lokale MMDB: Lookup passiert auf unserem Server, die IP verlässt unsere Infra nie. Kein DPA, kein Subprozessor. Passt zur Self-Host-Linie (Meilisearch/mCaptcha).

Wofür Geo-IP (Zweck): „Login aus Land X" + Anomalie „Login aus neuem Land" in der Session-/ Security-Sicht (P2); optionale Analytics-Anreicherung nach Land (Default aus); Feature-Flag-Targeting nach Land. Der HIBP-Breach-Check (EXT-12, password-policy.server.ts) ist davon unabhängig und bereits aktiv.

Speicher-/Minimierungs-Regeln:

  • IP nie im Klartext: IpAddress.ip/IpAddressLog.ip speichern HMAC-SHA256(IP, IP_HASH_PEPPER). Der Pepper liegt in den Coolify-Secrets, nicht in der DB → ein DB-Leak allein ist wertlos; der Gleichheits-Vergleich („gleicher Client?") bleibt möglich (Pseudonymisierung statt Anonymisierung).
  • Geo nur Land: regionCode/city/zipCode/latitude/longitude bleiben leer/null.
  • Der Blacklist-Abgleich nutzt die Roh-IP nur zur Laufzeit (Security-Liste), persistiert sie nicht im Log.

Provisionierung: Die MMDB ist ein Deploy-Artefakt (nicht im Git), Pfad via GEOIP_DB_PATH (Default ./data/geoip/dbip-country-lite.mmdb); scripts/update-geoip-db.sh lädt die Monatsversion. Fehlt die Datei, ist die Land-Auflösung ein No-op (Stub) ohne Fehler. Reader: npm maxmind (MIT).

Env: IP_HASH_PEPPER (Secret), GEOIP_DB_PATH (optional).

Folge-Arbeit (offen): monatlicher Worker-Cron (BullMQ) statt Shell-Skript; IpAddress/IpAddressLog perspektivisch auf Land + Hash-IP eindampfen (Spalten entfernen, sobald Session-Filter/Feature-Flag- Konsumenten umgestellt sind); UI, die bisher die Roh-IP zeigte, auf Land umstellen. Betrifft: EXT-12, P2 (Sessions/Security), P21 (DSGVO/Retention), modules/ipAddress, services/auth/password-policy. Anker: apps/web/app/modules/ipAddress/, docs/ARCHITECTURE-DECISIONS.md.

AD-23 — Media-Auslieferung: privater Bucket + authentifizierter App-Proxy (kein public-read) ✅

Entscheidung (2026-06-17): Hochgeladene Medien werden nicht über public-read S3 ausgeliefert. Der Bucket bleibt privat; die App liefert Medien über eine authentifizierte Proxy-Route aus (Session + Tenant-Scope + Permission), die das Objekt aus dem privaten Bucket streamt bzw. auf eine kurzlebige signierte Download-URL weiterleitet. STORAGE_PUBLIC_BASE_URL bleibt leer; der publicUrl- Pfad (RowHelper/MediaHelper/ApiHelper, publicUrl ?? file) zeigt auf die Proxy-Route.

Warum: CRM-Medien enthalten Kundendaten/Dokumente — world-readable Buckets sind DSGVO-untragbar (jeder mit der URL käme ohne Auth/Tenant-Check ran). Weg A (public-read) ist damit verworfen.

Für P4 (Files, AK-P4a/b) umzusetzen:

  • Proxy-Route (z.B. /media/… bzw. /storage/…) mit Auth + withTenantScope() + withPermission(), streamt aus dem privaten Bucket oder redirected auf signierte Download-URL (kurze TTL).
  • publicUrl in den Media-Helpern auf die Proxy-Route verdrahten (statt null/rohem Key).
  • Genuin öffentliche Assets (E-Mail-Bilder, öffentliche Formular-/Booking-/Portal-Seiten, Branding-Logos) können keinen Auth-Proxy nutzen → signierte URLs mit TTL oder ein separater public/-Prefix mit eng begrenztem public-read. Pro Asset-Typ entscheiden.
  • DSGVO-Export-Artefakte nutzen bereits signierte URLs (unverändert).

Betrifft: P4 (Files), EXT-06 (nur Bucket/Infra), packages/storage, apps/web Media-Helper. Anker: AD-8 (Verschlüsselung), packages/storage, apps/web/app/utils/helpers/MediaHelper.ts.


Phase-0-Pflichten (kein „Entscheid", sondern Voraussetzung — siehe _SUMMARY.md)

Diese Punkte sind vor der ersten Fachmodul-Umsetzung abzuarbeiten:

  1. Boilerplate als Code-Basis einziehen (Repo ist heute Docs-only).
  2. Naming-Drift beheben: @toolctrl/* → @hivegrid/*, ui-app-Stub → ui-hivegrid, Sidebar-Konvention, falsche AGENTS.md-Pfade (indexed-entities.ts, sidebar.config.ts).
  3. EU-Sterilisation: GitHub/Google-OAuth-Reste + User.githubId/googleId entfernen.
  4. M00 aktivieren + 4 echte Neubauten: logActivity()/Activity-Stream, Search-indexed-entities-Registry, Subframe-Adapter-Code (Quer-Voraussetzung für jede UI-Route), Mass-Action-Schutz.
  5. Fehlende Dependencies ergänzen (PLAN.md teils falsch): signature_pad, browser-image-compression, react-grid-layout, @dnd-kit (statt react-beautiful-dnd), libphonenumber, ics/ical-generator, btree_gist/EXCLUDE-Constraint u.a.

AD-24 — Prod unter eigenem Hetzner-Account (dediziert, self-contained) ✅

Entscheidung (2026-06-19, Owner Raffael): Produktion läuft in einem eigenen, dedizierten Hetzner-Cloud-Account, vollständig getrennt von Staging/dev. Topologie — 5 VMs (fsn1, x86 cx-Linie):

  • control — eigenes Coolify + Orchestrierung/Bastion (cx33: 4 vCPU/8 GB)
  • web, api (je cx33)
  • worker + services — 5 Worker-Queue-Gruppen + self-hosted Meilisearch + ClamAV + mCaptcha (cx43: 8 vCPU/16 GB)
  • db — Postgres + PgBouncer + Redis + Daten-Volume (cx33 + Volume)

mCaptcha nutzt die Postgres/Redis der db-VM (eigene DB), keine separate VM. Self-contained: eigene Zentral-Dienste (kein Zugriff aufs Staging-Privatnetz), eigener DEK-Master-Key (AD-8) + eigener S3-Bucket (hivegrid-prod, fsn1), eigenes Coolify.

Ingress (Variante „Cloudflare-Proxy"): Cloudflare proxied vor app./api./captcha. (Edge-TLS, DDoS/WAF, Origin-IP versteckt) → Coolify/Traefik pro Host (Let's-Encrypt am Origin). admin. restriktiv (dns-only + Firewall bzw. Cloudflare Access). Kein Hetzner-Load-Balancer bis zur horizontalen Skalierung — die App ist über serverseitige Sessions in Postgres+Redis (AD-4) dafür bereit; LB + 2+ web/api-Instanzen kommen nach Anhebung des VPS-Limits.

Netzwerk: privates Hetzner-Network + Firewall — db/services nur privat erreichbar, öffentlich nur 80/443 (web/api/control), SSH eingeschränkt.

Warum: Blast-Radius/Isolation (getrennte Tokens/Billing, kein Cross-Deploy-Unfall Staging↔Prod), saubere DSGVO-/Audit-Grenze, Crypto-Shredding-Grenze pro Account. Revidiert/erweitert die Infra-ADR-009 (Hetzner+Coolify) um die Account-Trennung.

Grenzen: Hetzner-Default-Limit 5 VPS/Projekt → echtes HA (web1/web2 + api1/api2 hinter LB) erst nach Limit-Anhebung (Support). Provisionierungs-Schritte + Go-Gates: docs/ops/prod-rollout-hivegrid.md.

Betrifft: Prod-Deploy, Ops, alle Module.

AD-25 — E-Signatur: gehärtete SES-Eigenbau, kein externer Provider (v1); QES-Add-on via DE-QTSP (v2) ✅ (NEU 2026-06-21)

Entscheidung (2026-06-21, Owner Raffael): HiveGrid baut die elektronische Signatur (Angebote/Aufträge/Abnahmen) für v1 selbst als gehärtete SES und macht das zum Default für alle Tenants — kein externer Provider als Zwang. PandaDoc bleibt als optionale BYOK-Anbindung erhalten: Tenants mit eigenem PandaDoc-Account tragen ihren eigenen API-Key ein und signieren über ihren Account (Opt-in). Damit ist die Annahme „PandaDoc ist der zentrale/erzwungene Provider" abgelöst (Abweichung vom Pflichtenheft V.4.4), PandaDoc selbst aber nicht — es wird vom Default zum Opt-in-Adapter. Betrifft AK-P16d, EXT-09, EXT-17, VR-P16.

BYOK-Vorteil (CLOUD Act): Bei BYOK nutzt der Tenant bewusst seinen eigenen PandaDoc — die CLOUD-Act-/Drittland- Verantwortung liegt damit beim Tenant (dessen Account, dessen Entscheidung), nicht bei HiveGrid. Das „100 % EU"- Default (SES-Eigenbau) bleibt unangetastet; PandaDoc ist eine transparente Opt-in-Wahl (Subprozessor-Liste/AVV).

Begründung (dreifach belegt):

  • Rechtlich: SHK-Dokumente sind formfrei oder Textform (§§ 631, 650i Abs. 2, 640 BGB) → SES genügt (auch beim Verbraucherbauvertrag, der nur Textform verlangt — keine QES). Einziger zwingender QES-Fall im ganzen SHK: Kündigung eines Bauvertrags (§ 650h BGB) — kein Tagesgeschäft, selten.
  • Praxis: Marktstandard im DE-Handwerk ist die SES-Touch-Unterschrift vor Ort (+ Fern-Link). Alle etablierten Tools (HERO, plancraft, Craftnote, ToolTime …) bieten SES; AES/QES spielt im Tagesgeschäft keine Rolle, kein Marktdruck. Wettbewerbsfaktor ist der Prozess (papierlos → schneller zur Rechnung), nicht die Stufe.
  • Souveränität: PandaDoc/DocuSign = US-Firmen → CLOUD Act §2713, bricht das „100 % EU"-Versprechen (A.12.4). EU-Provider geprüft (Yousign FR am besten; Skribble CH hat US-Subprozessoren/kein echtes Embedding/per-Seat). OSS geprüft (Documenso = nur SES + AGPL). Eigenbau ist souveränster + günstigster + rechtlich ausreichend.

Umsetzung v1 (P16 / AK-P16d) — „gehärtete SES" (praktisch auf AES-Beweisniveau, ohne PKI-pro-Person/QTSP):

  1. Touch-Signatur vor Ort (Canvas → PDF) + Fern-Link-Signatur (Angebot remote annehmen)
  2. OTP-Identifizierung (E-Mail; SMS als stärkere Zwei-Kanal-Variante)
  3. PDF-Versiegelung (PAdES mit HiveGrid-Organisations-Zertifikat → jede Änderung bricht die Signatur)
  4. Zeitstempel (TSA, RFC 3161; nicht-qualifiziert genügt für SES/AES)
  5. Audit-Trail über M001 (Link/Öffnen/OTP/Signatur: wer, wann, IP, Gerät, Geo, PDF-Hash; append-only)
  6. Completion-Certificate als PDF-Anhang (sichtbare Beweis-Doku)
  7. Einmal-Link (personengebunden) + expliziter „Intent to sign"

Beweiskraft: gehärtete SES wie AES unter freier Beweiswürdigung (§ 286 ZPO) — der Audit-Trail trägt, nicht das Stufen-Etikett. Anscheinsbeweis (§ 371a) hat nur QES.

Architektur: Signatur über defineAdapter (Apps-Engine, provider-agnostisch). Adapter:

  • ses-selfhosted — Default (v1), die gehärtete Eigenbau-SES.
  • pandadoc-byok — optional (v1), Tenant bringt eigenen PandaDoc-API-Key (DEK-verschlüsselt im Credential-Store); Mandanten-Trennung via eigener Account + Webhook-Routing (pro-Tenant-URL + metadata.hivegrid_tenant_id). Anbindungs-Spec: EXT-17.
  • Spätere Optionen ohne Fachcode-Umbau: AES (Yousign managed oder EU-DSS-Library self-hosted, Java-Sidecar), QES (DE-QTSP D-Trust sign-me, Hash-only — Dokument bleibt im Perimeter) für § 650h-Bauvertragskündigung.

Betrifft: P16 (AK-P16d), EXT-09 (SES-Eigenbau-Default), EXT-17 (PandaDoc-BYOK-Adapter), VR-P16. PandaDoc bleibt als BYOK-Opt-in. Vor Produktivschaltung der Signatur: anwaltliche Bestätigung der Form-/Beweis-Einordnung (SES genügt, § 650h→QES).

AD-26 — KI-Pivot: MCP-only, kein selbst-gehostetes LLM/RAG (löst die KI-Hälfte von AD-16 ab) ✅ (NEU 2026-06-22)

Entscheidung (2026-06-22, Owner Raffael): HiveGrid hostet kein eigenes LLM/RAG/HiveMind mehr. Statt eine KI-App zu sein, wird HiveGrid KI-anschließbar über einen MCP-Server (Model Context Protocol): externe KI-Clients (Claude Desktop/Cursor/ChatGPT) docken an, Inferenz liegt beim Client. Damit entfällt der EU-LLM-Provider-/DSGVO- Zwang für HiveGrid selbst (kein Mistral/Scaleway/AlephAlpha-Hosting mehr). Löst die KI-Hälfte von AD-16 ab — die Automation-Säule bleibt.

Begründung: Kleines Team; der teure/harte Teil (LLM-Hosting, RAG-Infra, Promptfoo-Eval, AiCredits-Abrechnung, EU-Provider-Compliance) fällt weg. MCP nutzt dieselbe mandanten-gescopte Daten-/Aktions-Fläche wie die geplante Public-API (VR-P22a/VR-P22b) → keine Doppelarbeit.

v1 (KI komplett gestrichen): R-P13 (KI-Layer/Mistral-Gateway) + D-P13 entfernt, EXT-11 + Mistral-Key gelöscht, Milestones M083–M088 gelöscht, R-P12b verliert den Sprach→Automation-Teil (bleibt reiner visueller Flow-Editor). Neues letztes v1-Glied = R-P12b (~1 Tag Puffer → Härtung). Freeze/Launch 09./10.07. bleiben fix.

v2: VR-P25/VD-P25 (HiveMind/RAG/Whisper) gestrichen (Plane: Cancelled) → neu VR-MCP + VD-MCP im Slot 18.–20.07., blocked-by VR-P22a/b (Public-API, fertig 17.07. → kein Konflikt).

MCP-Design (Best-Practice, MCP-Spec-Rev 2025-11-25): Remote-Server über Streamable HTTP (stateless Start), SDK @modelcontextprotocol/sdk (Gerüst apps/web/app/modules/mcp/ existiert bereits). Auth Stufe 1 = API-Key-Bearer (aus VR-P22), Stufe 2 = OAuth 2.1 (PKCE/RFC 9728/8707) später. Tenant aus dem Token (nie aus Tool-Argumenten) → withTenantScope() + RLS. Read-Tools + Write-Tools (withPermission() + Client-Confirmation + auditEvent/logActivity). Annotations (readOnlyHint/destructiveHint) nur als Hint — echte Guards serverseitig. Rate-Limit + Output-Limits + Pagination; Tenant-Daten sind Daten, nie Instruktionen (Prompt-Injection).

Code-Rip-out (Variante a): raus = packages/ai, packages/vector-store, Module ai/ai-gen/ai-cost/chatbot/ ai-elements, AI-Routen, Worker-AI-Jobs, ai.prisma — UND die Legacy-PromptFlow-Engine (promptBuilder/admin/prompts/ promptFlows.prisma). Bleiben: @modelcontextprotocol/sdk, modules/mcp/, der EU-Sterilisations-Guard.

Pflichtenheft: A.12.3 (EU-LLM-Provider) und V.2.2 („KI als Bedienoberfläche der Automation") widersprechen jetzt → Change-Request (M176) nötig; bis dahin gilt diese AD vor dem Pflichtenheft (Pivot-Vorrang). Anti-Liste V.3.2.7 (HiveMind autonom) bleibt erfüllt (wird gar nicht gebaut).

Betrifft: R-P13/D-P13/EXT-11/M083–M088 (entfernt), R-P12b (Trim), VR-P25/VD-P25 (gestrichen), VR-MCP/VD-MCP (neu), R-P11a/R-P11b/D-P3/R-P5/D-P5/D-P20 (KI-Andock umdeuten/trimmen), VA-P24b/VA-P29/VR-P31/VR-P33 (KI-Teile entfallen).

KWP (separater Strang — siehe docs/boilerplate-mapping/M19-kwp-integration.md)

KWP bekommt eine offizielle API-Anbindung → M089 (Windows-Sync-Programm) entfällt, M090/M091/M092/M094/M095 vereinfachen sich, M093 bleibt. KWP als DataConnection type='rest-api' ins Apps-Engine-Muster (M33a). M089–M095 vor Umsetzung neu schneiden.