451 lines
32 KiB
Markdown
451 lines
32 KiB
Markdown
# 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.
|