XOLIB Soll-Schema — Datenarchitektur Blaupause¶
Version: 1.6 Datum: 2026-03-21 Status: Referenzdokument — jedes Feature wird gegen dieses Schema gebaut Aktueller Stand (Soll): 109 Models, 30 Enums, ~2400 Felder, Qualität 7/10 Ist-Stand (22.03.2026): 140 Models, 65 Enums, 4990 Schema-Zeilen Review: Extern bewertet — "Für Pre-Launch außergewöhnlich stark", 6 strategische Lücken identifiziert
Prinzipien¶
- Single Source of Truth — Jeder Datenpunkt hat genau einen Ort. Keine Doppelpflege.
- Historisierung — Jede Änderung wird protokolliert. Wir wissen nicht nur WAS ist, sondern WAS WAR.
- AI-First — Jedes Model muss Daten liefern die der KI helfen. Mehr Felder = bessere Vorhersagen.
- Data Moat — Jeder Mandant macht das System schlauer. Event Sourcing + Feedback Loops + Cross-Tenant Benchmarks.
- Workflow-Moat — Daten allein sind kein Moat. Wer den operativen Workflow kontrolliert (Zahlungsfluss, Compliance, Dokumentation), macht den Wechsel schmerzhaft — nicht wegen Features, sondern wegen operativer Abhängigkeit.
- DSGVO by Design — PII verschluesselt, Loeschfristen definiert, k-Anonymität bei Aggregation.
- English Field Names — Code ist Englisch. Deutsche Begriffe nur in Enums wo fachlich noetig.
- Enums statt Strings — Jedes Feld mit festen Werten bekommt ein Enum.
- Kein Binary in DB — Bilder und PDFs in Object Storage, nur URLs in der DB.
- Max 30 Felder pro Model — Groessere Models werden gesplittet. JSON nur fuer volatile Settings.
- Jedes Model hat tenantId + RLS — Multi-Tenancy wird auf DB-Ebene erzwungen — tenantId auf jedem Model + PostgreSQL Row-Level Security. App-Code kann vergessen zu filtern, RLS nicht.
- Platform-First — Xolib ist nicht nur Software, sondern Infrastruktur auf der andere bauen (API-as-Product, Webhooks, OpenImmo).
- Human-in-the-Loop — KI empfiehlt, der Mensch entscheidet. Kein AI-Agent darf eine Aktion mit rechtlicher oder finanzieller Wirkung auf eine natuerliche Person automatisch ausfuehren (Art. 22 DSGVO). Jede solche Aktion erfordert
requiresHumanApproval: true. Verstoesse: bis zu 20 Mio EUR oder 4% Jahresumsatz. - PII-Stripping vor LLM-Calls — Alle AI-Agent-Calls die personenbezogene Daten enthalten, muessen vor dem API-Call durch eine PII-Stripping-Middleware laufen. Mieternamen → Pseudonyme, IBANs → maskiert, Adressen → PLZ-Ebene. Zuordnung bleibt lokal bei Xolib. Loest DSGVO-Compliance UND Vendor-Lock-in.
Loesch-Strategie (Soft-Delete-Standard)¶
| Datentyp | Strategie | Beispiele |
|---|---|---|
| Stammdaten | Soft-Delete (isDeleted, deletedAt, deleteReason) |
Property, Unit, Owner, User |
| Transaktionsdaten | NIEMALS loeschen (§257 HGB, 7 Jahre) | Payment, BankTransaction, Lease |
| Kommunikation | Soft-Delete, nach DataRetentionPolicy pseudonymisieren | Ticket, Message, Letter |
| Events | Append-only, archivieren nach 12-24 Monaten | XolibEvent, ChangeLog, AuditLog |
| AI-Daten | Behalten (Training Data) | AgentFeedback, AgentOutcome, EntityMemory |
| Bilder/Dokumente | Soft-Delete, nach 90 Tagen physisch loeschen | PropertyImage, PropertyDocument |
Cascade-Regeln: - Property geloescht → Units werden NICHT geloescht (Soft-Delete auf Property reicht) - Unit geloescht → Pruefen ob aktiver Lease existiert (blockiert) - User geloescht → Pseudonymisierung: Name→"Geloeschter Nutzer", Email→hash, Phone→null - Tenant geloescht → Alles loeschen (CASCADE) — Mandant komplett weg
Schema-Evolution (Zero-Downtime-Migrationen)¶
- Neue Felder immer optional (nullable) — nie required in einem Schritt
- Zwei-Phasen-Deploys:
- Phase 1: Schema-Migration (neues Feld hinzufuegen, optional)
- Phase 2: Code-Deploy (nutzt neues Feld, schreibt Defaults)
- Phase 3: Cleanup (altes Feld entfernen — erst wenn alle Daten migriert)
- Enum-Erweiterungen: Neue Werte hinzufuegen ist safe. Werte umbenennen/entfernen NIE ohne Migration.
- Prisma db push fuer Dev,
prisma migrate deployfuer Prod — niemigrate devauf Prod - Backup vor jeder Migration —
pg_dumpvor jedem Schema-Change auf dem Server
Domains & Models¶
Domain 1: CORE (Tenant, User, Auth)¶
Tenant (ERWEITERN — Lifecycle + Settings)¶
- Aktuell: 65+ Relations,
settingsJSON catch-all - Aenderung:
settingsJSON bleibt, aber dokumentiert welche Keys existieren - Neue Felder (Lifecycle):
status(Enum: TRIAL, ONBOARDING, ACTIVE, CHURNING, SUSPENDED, DELETED)trialEndsAt DateTime?onboardedAt DateTime?— wann Import abgeschlossenchurnRiskScore Float?— 0.0-1.0, berechnet aus Nutzungsdaten (Feature Store)lastActiveAt DateTime?— letzter Login irgendeines Users- Prioritaet: Phase C (Churn-Prevention)
User (SPLITTEN)¶
- Aktuell: 50+ Felder — Auth + Profil + Demographie + Praeferenzen gemischt
- Soll:
| Model | Felder | Zweck |
|---|---|---|
User |
id, tenantId, email, passwordHash, totpSecret, role, isActive, lastLoginAt, lang | Auth & Identity |
UserProfile |
userId, name, phone, dateOfBirth, nationality, incomeRange, householdType, photo | Persoenliche Daten (PII — verschluesselt) |
UserPreferences |
userId, preferences (JSON) | UI-Einstellungen, KPI-Auswahl, Insight-Settings |
- Migration: UserProfile und UserPreferences aus bestehenden User-Feldern befuellen
- Prioritaet: Bei Personen-Modul V6
Owner (ERWEITERN)¶
- Aktuell: 9 Felder, IBAN unverschluesselt
- Aenderung:
bankAccountIBANverschluesseln (AES-256-GCM wie BankAccount) - Neue Felder:
ownerType(PRIVAT, GBR, GMBH, WEG, ERBENGEMEINSCHAFT),taxId,ustIdNr - Prioritaet: Bei Finanzen-Modul
Domain 2: PROPERTY (Gebaeude & Einheiten)¶
Property (SPLITTEN — 75 Felder → 3 Models)¶
| Model | Felder | Zweck |
|---|---|---|
Property |
id, tenantId, propertyNumber, name, street, zipCode, city, stadtteil, bundesland, latitude, longitude, propertyType (Enum), houseType (Enum), objectCondition (Enum), buildingYear, isNeubau, buildingPhase, isDenkmalschutz, isDeleted, deletedAt, deleteReason, imageUrl, notes, ownerId, verwaltungsArt, verwaltungsgebiet | Stammdaten & Standort |
PropertyBuilding |
propertyId (1:1), totalFloors, plotSizeSqm, totalHeatedSqm, roofType, facadeType, windowType, parkingSpaceCount, garageSpaces, elevatorAvailable, isBarrierFree, hasDisabledParking, entranceWidthCm, hasFiber, internetProvider, internetMaxSpeed, hasAC, ventilationType, gardenAvailable, lastRenovationYear, roofRenovationYear, windowsRenovationYear | Gebaeudedaten & Technik |
PropertyEnergy |
propertyId (1:1), heatingType (Enum), heatingYear, heatingBrand, heatingNotes, energyCarrier (Enum), warmWaterType, hasWarmWaterCirculation, energyAusweisType, energyValue, energyEfficiencyClass, energyAusweisExpiry, hasSolarPanels, solarKwp, hasEVCharger, hasSmartMeter | Energie & Heizung |
PropertyFinance |
propertyId (1:1), bankAccountIBAN (encrypted), bankAccountBIC, bankName, grundsteuerAmount, gebaeudeversicherung, hausgeld, mietspiegel, bodenrichtwert | Finanzen & Verwaltung |
- Migration: Felder aus Property in neue 1:1 Models kopieren, API-Response bleibt flach (JOIN)
- Prioritaet: JETZT (Properties V6 laeuft gerade)
Unit (BEREINIGEN — Equipment normalisieren)¶
| Model | Felder | Zweck |
|---|---|---|
Unit |
id, tenantId, propertyId, number, floor, position, sqm, rooms, unitType (Enum), condition, mea, notes | Stammdaten |
UnitEquipment |
unitId (1:1), hasBalcony, sqmBalcony, hasTerrace, sqmTerrace, hasGarden, sqmGarden, hasKeller, sqmKeller, hasEBK, hasGuestWC, hasAbstellraum, hasWashingConn, hasStellplatz, bathroomCount, floorType, windowType, ceilingHeight, loadCapacity | Ausstattung |
UnitAccessibility |
unitId (1:1), isBarrierFree, doorWidthCm, hasFlushShower, hasGrabBars, hasShowerSeat | Barrierefreiheit |
UnitRentalTerms |
unitId (1:1), isWBS, furnishing (Enum), petsAllowed (Enum), lastRenovation | Mietkonditionen |
- Deprecated Felder auf Unit (Schritt 4):
currentRent,nkVorschuss,currentTenantId→ kommen aus Lease - Migration: Equipment-Felder in UnitEquipment kopieren
- Prioritaet: JETZT (Einheiten-Tab wird gerade gebaut)
ParkingSpace (NEU — bereits erstellt ✅)¶
- Eigenes Model fuer Stellplaetze mit Nummer, Typ, Miete, EV-Charger
- Relation zu Property + optional Unit
PropertyImage (AENDERN — Binary raus)¶
- Aktuell:
data Bytesin DB - Soll:
url String→ Object Storage (S3/Cloudflare R2) - Prioritaet: Spaeter (funktioniert, nur Skalierbarkeit)
Domain 3: LEASE & PAYMENTS (Mietvertraege & Zahlungen)¶
Lease (SPLITTEN bei Bedarf)¶
- Aktuell: 40+ Felder — Wohnraum + Gewerbe gemischt
- Soll: Vorerst beibehalten — die Trennung Wohnraum/Gewerbe ueber
leaseTypeEnum funktioniert - Neue Felder: Keine noetig
- Wichtig:
rentIncreaseHistoryJSON → eigenes Model
RentAdjustment (NEU)¶
Model RentAdjustment {
id, leaseId, tenantId
type: Enum (INDEX, STAFFEL, MIETSPIEGEL, VERGLEICHSMIETE, MODERNISIERUNG)
previousRent, newRent
effectiveDate
reason, legalBasis (z.B. "§558 BGB")
status: Enum (BERECHNET, ANGEKUENDIGT, WIRKSAM, WIDERSPRUCH)
notificationDate, notificationLetterUrl
createdAt, createdBy
}
Payment (ERWEITERN)¶
- Neue Felder:
matchCorrected Boolean,matchCorrectedAt DateTime(Feedback fuer Bank-Matching-KI) - Prioritaet: Beim Banking-Modul V2
Domain 4: HISTORISIERUNG (NEU — Kernstück des Umbaus)¶
ChangeLog (NEU — JETZT BAUEN)¶
Model ChangeLog {
id String @id @default(cuid())
tenantId String
entityId String
field String // "baseRent", "energyClass", "condition"
oldValue String? // JSON stringified
newValue String? // JSON stringified
changedBy String // userId
changedAt DateTime @default(now())
reason String? // "Mietanpassung Index 2026", "Sanierung abgeschlossen"
source ChangeSource // Enum: API, IMPORT, AGENT, MIGRATION, WEBHOOK, SYSTEM, MANUAL
entityType ChangeEntityType // Enum: PROPERTY, UNIT, LEASE, PAYMENT, OWNER, TICKET, etc.
containsPII Boolean @default(false) // DSGVO: true wenn personenbezogene Daten betroffen → faellt unter Loeschanfragen
@@index([tenantId, entityType, entityId])
@@index([tenantId, changedAt])
@@index([entityType, field])
}
Partitionierung & Archivierung:
- PostgreSQL Native Partitioning nach changedAt (quartalsweise)
- Archivierung nach 24 Monaten in ChangeLogArchive (gleiche Struktur, separate Tabelle)
- Retention an DataRetentionPolicy koppeln
- Geschaetztes Volumen: ~100 Mio Eintraege/Jahr bei 10.000 Mandanten
Abgrenzung ChangeLog ↔ XolibEvent: - XolibEvent = "Was ist passiert?" (Domain-Ebene) — z.B. "Mieter hat Ticket erstellt", "Agent hat Mietanpassung empfohlen" - ChangeLog = "Was hat sich geaendert?" (Daten-Ebene) — z.B. "baseRent: 650 → 690", "energyClass: D → C" - Beide zusammen ergeben den vollstaendigen Kontext fuer die KI: Das Event erklaert das WARUM, der ChangeLog zeigt das WAS
Domain 5: CROSS-TENANT INTELLIGENCE (NEU)¶
SystemBenchmark (NEU)¶
Model SystemBenchmark {
id String @id @default(cuid())
metricKey String // "operating_costs_sqm", "vacancy_rate", "payment_ratio"
period String // "2026-Q1", "2026-03"
cohortCity String? // "Berlin", "Muenchen" (generalized)
cohortType String? // "MFH", "EFH" (building type)
cohortSize String? // "KLEIN", "MITTEL", "GROSS" (unit count class)
tenantCount Int // Must be >= 5 (k-anonymity)
sampleCount Int
mean Float
median Float
p10 Float
p25 Float
p75 Float
p90 Float
stdDev Float?
expiresAt DateTime? // createdAt + 2 Quartale — danach ungueltig
isStale Boolean @default(false) // true wenn Kohorte unter k faellt
createdAt DateTime @default(now())
@@unique([metricKey, period, cohortCity, cohortType, cohortSize])
}
SystemModelParam (NEU)¶
Model SystemModelParam {
id String @id @default(cuid())
modelKey String // "bank_matching_weights", "score_normalization"
version Int
parameters Json // { amount: 0.35, date: 0.20, reference: 0.25, name: 0.20 }
trainingSize Int
accuracy Float?
validFrom DateTime
validUntil DateTime?
isActive Boolean @default(true)
createdAt DateTime @default(now())
@@unique([modelKey, version])
}
SystemTemplate (NEU)¶
Model SystemTemplate {
id String @id @default(cuid())
templateType String // "MAHNUNG", "MIETERHOHUNG", "WILLKOMMEN"
templateKey String // "mahnung_stufe1_freundlich"
content String
usageCount Int @default(0)
successRate Float? // 0.0-1.0
avgOutcomeDays Float? // Days until positive outcome
tenantCount Int @default(0)
isRecommended Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([templateType, templateKey])
}
TenantTemplate (NEU — mandantenspezifische Overrides)¶
Model TenantTemplate {
id String @id @default(cuid())
tenantId String
systemTemplateId String? // NULL = komplett eigenes Template
templateType String
templateKey String
content String // Mandanten-Override
usageCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([tenantId, templateType, templateKey])
}
- Prioritaet: Phase F (Cross-Tenant Templates)
Domain 6: EVENT SOURCING & AI¶
XolibEvent (BEHALTEN — gut aufgebaut ✅)¶
- Append-only, 52+ Event-Typen
- Erweiterung: Archivierung nach 12 Monaten in Cold Storage
ChangeLog (siehe Domain 4)¶
AgentFeedback + AgentOutcome (BEHALTEN + ERWEITERN)¶
- Feedback Loops funktionieren
- Erweiterung AgentOutcome:
eurDelta Float?— ROI-BerechnungrequiresHumanApproval Boolean @default(false)— Art. 22 DSGVO PflichtapprovedBy String?— userId des genehmigenden SachbearbeitersapprovedAt DateTime?autoExecuted Boolean @default(false)— true = ohne menschliche Freigabe ausgefuehrtmodelVersion Int?— welche Modell-Version hat entschieden (A/B Testing)experimentId String?— welches Experiment laeuft- Art. 22 Risiko-Klassifizierung:
- HOCH: Bank-Matching → Mahnung, Mietanpassung → Mieterhoehung, Churn-Score → Kuendigung
- MITTEL: Ticket-Eskalation → Handwerkerkosten, Xolib Score → externe Kreditentscheidung
- NIEDRIG: Ticket-Kategorisierung, Uebersetzung, Zusammenfassung
Episodic Memory (NEU — Phase 2)¶
Model EntityMemory {
id String @id @default(cuid())
tenantId String
entityType String // "Property", "Unit", "Tenant", "Owner"
entityId String
memoryType String // "EPISODIC", "SEMANTIC", "PROCEDURAL"
memoryKey String // "sanierung_2024", "mieterstreit_2025", "general"
content String // Compressed text summary
containsPII Boolean @default(false) // DSGVO: true wenn Mieterdaten enthalten → Loeschanfragen
embedding Unsupported("vector(1536)")?
confidence Float?
hitCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([tenantId, entityType, entityId, memoryType, memoryKey])
@@index([tenantId, entityType])
}
Domain 7: TICKETS & KOMMUNIKATION (BEREINIGEN)¶
Ticket (BEHALTEN — gut aufgebaut ✅)¶
- Aenderung:
ticketNumberauf@@unique([tenantId, ticketNumber])umstellen - Prioritaet: Beim Nummern-Umbau
Message (LEICHT AENDERN)¶
translationDEFeld entfernen →translatedText+targetLangreicht- Prioritaet: Niedrig
Domain 8: BETRIEBSKOSTEN & HEIZKOSTEN (BEHALTEN)¶
- OperatingCostSettlement + Item + UnitResult: Gut normalisiert ✅
- HeatingCostSettlement + UnitResult: Gut, CO2-Allokation korrekt ✅
- Aenderung:
OperatingCostItem.costTypeString → Enum - Prioritaet: Beim BK-Modul V2
Domain 9: HANDWERKER & SERVICE (BEHALTEN)¶
- ServiceProvider, ServiceOrder, SchedulingRequest: Gut aufgebaut ✅
- Aenderung:
ServiceProvider.tradeString → Enum (aus TicketCategory) - Neues Feld:
globalProviderId String?— Verknuepfung zu einem globalen Handwerker-Profil (mandantenuebergreifend). Ermoeglicht: "Firma Mueller arbeitet fuer 12 Verwaltungen — Reaktionszeit 2.3 Tage, Folgeticket-Rate 8%" - Prioritaet: globalProviderId bei Phase C, trade Enum niedrig
Domain 10: WEG (BEHALTEN)¶
- 9 WEG-Models: Sehr gut aufgebaut ✅
- Hausgelder, Beschluesse, Wirtschaftsplaene, Jahresabrechnungen
- Keine Aenderungen noetig
Domain 11: GEWERBE (BEHALTEN)¶
- 5 Gewerbe-Models: Index, Staffel, Umsatz, Kaution, Nebenkosten
- Aenderung:
GewerbeNebenkostenConfig.includedCostsJSON → normalisieren - Prioritaet: Beim Gewerbe-Modul V2
Domain 12: XOLIB SCORE (BEHALTEN — exzellent ✅)¶
- Hash-Chaining, Tamper Detection, 5-Kategorien-Modell
- Erweiterung:
benchmarkMedian,benchmarkPercentileauf PropertyScore - Prioritaet: Bei Cross-Tenant Phase 1
Domain 13: BANKING (BEHALTEN)¶
- BankAccount, BankTransaction: Gut ✅
- Aenderung:
counterpartIbanverschluesseln - Erweiterung:
Payment.matchCorrectedfuer Modell-Training - Prioritaet: Bei Banking V2
Domain 14: GDPR & COMPLIANCE (ERWEITERN)¶
DataRetentionPolicy (NEU)¶
Model DataRetentionPolicy {
id String @id @default(cuid())
entityType String // "User", "Payment", "Ticket"
retentionDays Int // 2555 (7 Jahre fuer Buchhaltung)
pseudonymizeAfter Int? // Tage bis Pseudonymisierung
deleteAfter Int? // Tage bis Loeschung
legalBasis String // "§257 HGB", "Art. 17 DSGVO"
isActive Boolean @default(true)
}
DataExportRequest (NEU)¶
Model DataExportRequest {
id String @id @default(cuid())
tenantId String
userId String
status String // REQUESTED, PROCESSING, READY, EXPIRED
format String // JSON, CSV
fileUrl String?
requestedAt DateTime @default(now())
completedAt DateTime?
expiresAt DateTime?
}
AdminAction (NEU — Super-Admin Audit Trail)¶
Model AdminAction {
id String @id @default(cuid())
adminUserId String
targetTenantId String?
actionType String // "IMPERSONATE", "DATA_EXPORT", "TENANT_SUSPEND", "CONFIG_CHANGE"
details Json?
ipAddress String?
createdAt DateTime @default(now())
@@index([adminUserId, createdAt])
@@index([targetTenantId, createdAt])
}
DataRetentionExecution (NEU — Enforcement-Nachweis)¶
Model DataRetentionExecution {
id String @id @default(cuid())
policyId String
executedAt DateTime @default(now())
entityType String
recordsScanned Int
recordsDeleted Int
recordsPseudonymized Int
status String // "SUCCESS", "PARTIAL", "FAILED"
errorLog Json?
@@index([policyId, executedAt])
}
- Prioritaet: Vor Go-Live (rechtlich noetig)
Domain 15: SAAS & BILLING (BEHALTEN)¶
- Plan, Feature, Subscription, Invoice: Funktioniert ✅
- Aenderung:
Plan.featuresJSON →PlanFeatureJunction-Tabelle (spaeter)
Domain 16: LEGACY (ENTFERNEN)¶
ExternalFirm→ durch ServiceProvider ersetztWorkOrder→ durch ServiceOrder ersetzt- Aktion: Soft-Delete, dann nach 3 Monaten loeschen
- Prioritaet: Aufraeum-Sprint
Sicherheit — Sofort-Massnahmen¶
| Feld | Problem | Loesung | Prioritaet |
|---|---|---|---|
| Alle tenant-scoped Tables | Isolation nur per App-Code | PostgreSQL Row-Level Security Policies | HOCH |
Owner.bankAccountIBAN |
Unverschluesselt | AES-256-GCM (wie BankAccount) | HOCH |
BankTransaction.counterpartIban |
Unverschluesselt | AES-256-GCM | HOCH |
User.dateOfBirth |
Unverschluesselt | In UserProfile mit Encryption | MITTEL |
PropertyImage.data |
Binary in DB | S3/R2 mit URL | NIEDRIG (funktioniert) |
| Rate Limiting | Kein Echtzeit-Enforcement | Redis/in-memory, NICHT ueber DB-Queries | MITTEL |
Fehlende Indexes¶
| Query Pattern | Index | Prioritaet |
|---|---|---|
| Unbezahlte Mieten | Payment [leaseId, status, dueDate] |
HOCH |
| Offene Tickets nach Kategorie | Ticket [tenantId, category, status] |
HOCH |
| Units mit Mieter | Unit [propertyId, currentTenantId] |
MITTEL |
| Ablaufende Dokumente | PropertyDocument [tenantId, expiresAt] |
MITTEL |
| ChangeLog Abfragen | ChangeLog [tenantId, entityType, entityId] |
HOCH (neu) |
Migrations-Reihenfolge¶
Phase A: Historisierung (JETZT — vor allem anderen)¶
- ChangeLog Model erstellen
- API-Middleware: Automatisches Logging bei jedem Update
- Ab sofort wird jede Aenderung getrackt
Phase B: Properties Domain (JETZT — V6 Sprint)¶
- PropertyBuilding, PropertyEnergy, PropertyFinance Models
- UnitEquipment, UnitAccessibility, UnitRentalTerms Models
- Daten migrieren (additive INSERTs)
- API umstellen (JOINs, flache Response)
- Unit Deprecated Fields entfernen (Schritt 4)
Phase C: Cross-Tenant + EntityMemory (naechster Sprint)¶
- SystemBenchmark + SystemAggregationJob
- EntityMemory (Episodic, Semantic, Procedural) — PARALLEL, nicht danach
- Naechtliche SQL-Aggregation (5 Metriken)
- API: GET /api/v1/benchmarks/:metric
- KPI-Panels zeigen "Ihr Wert vs. Markt"
Phase D: Sicherheit (vor Go-Live)¶
- IBAN-Verschluesselung (Owner, BankTransaction)
- DataRetentionPolicy + DataExportRequest
- Fehlende Indexes
- Legacy Models entfernen
- Differential Privacy auf SystemBenchmark (Laplace-Noise)
Phase E: User Domain (bei Personen V6)¶
- User → User + UserProfile + UserPreferences splitten
- PII verschluesseln
Phase F: Erweiterte Intelligence¶
- SystemModelParam (gelernte Parameter) + A/B Testing
- SystemTemplate (Template-Performance)
- RentAdjustment Model
- BenchmarkAccess Model (API-Zugang fuer externe Partner — Daten-Monetarisierung)
Enums — Fehlende Definitionen¶
Diese String-Felder sollten Enums werden:
| Feld | Werte | Prioritaet |
|---|---|---|
ChangeLog.source (ChangeSource) |
API, IMPORT, AGENT, MIGRATION, WEBHOOK, SYSTEM, MANUAL | SOFORT (Phase A) |
ChangeLog.entityType (ChangeEntityType) |
PROPERTY, UNIT, LEASE, PAYMENT, OWNER, TICKET, USER, SERVICE_ORDER | SOFORT (Phase A) |
Property.propertyType |
WOHNUNG, HAUS, GEWERBE, GRUNDSTUECK | JETZT |
Property.houseType |
EFH, RH, DHH, MFH, VILLA, BUNGALOW | JETZT |
Property.objectCondition |
ERSTBEZUG, GEPFLEGT, SANIERT, RENOVIERUNGSBEDUERFTIG | JETZT |
Property.energyCarrier |
GAS, OEL, FERNWAERME, STROM, PELLETS, SOLAR | JETZT |
Unit.furnishing |
FURNISHED, PARTIALLY, UNFURNISHED | JETZT |
Unit.petsAllowed |
YES, NO, BY_ARRANGEMENT | JETZT |
PropertyComponent.category |
DACH, FASSADE, FENSTER, HEIZUNG, AUFZUG, etc. | MITTEL |
OperatingCostItem.costType |
WASSER, ABWASSER, MUELL, STROM, etc. | MITTEL |
ServiceProvider.trade |
HEIZUNG, SANITAER, ELEKTRIK, etc. | NIEDRIG |
Data Moat Checkliste¶
Jedes neue Feature muss diese Fragen beantworten:
- Welche Daten sammelt es? (Events, Stammdaten, Transaktionen)
- Wie fuettert es die KI? (Feedback, Outcomes, Training Data)
- Was lernt das System daraus? (Patterns, Benchmarks, Predictions)
- Was ist der Switching Cost? (Historische Daten die nur bei uns existieren)
- Welche Xolib Score Kategorie beeinflusst es? (Substanz, Technologie, Ertrag, Compliance, Instandhaltung)
- Ist es Cross-Tenant aggregierbar? (k-Anonymitaet + Differential Privacy, DSGVO-konform)
- Bindet es den Mandanten tiefer in einen operativen Workflow ein? (Zahlungsfluss, Compliance, Dokumentation — Workflow-Moat)
- Erzeugt es Daten oder Events die ueber Webhooks/API fuer Drittanbieter wertvoll sind? (Platform-Moat)
Feature Store Konzept (Architekturentscheidung)¶
Die Architektur definiert wo Daten gespeichert werden — aber auch wie sie in trainierbare Features transformiert werden. Ohne eine Zwischenschicht muss jeder Agent seine eigene Datenaufbereitung machen.
Konzept: Ein Feature Store produziert aus ChangeLog + XolibEvent + Stammdaten aggregierte, normalisierte Features:
Roh-Daten (ChangeLog, XolibEvent, Lease, Payment, ...)
↓ Naechtliche Aggregation (SystemAggregationJob)
Feature Store (EntityMemory + SystemBenchmark + SystemModelParam)
↓ Agents konsumieren normalisierte Features
KI-Entscheidungen (AgentRun → AgentOutcome → Feedback)
↓ Feedback fliesst zurueck
Bessere Features + Modelle
Kein eigenes Model noetig — der Feature Store ist die Kombination aus EntityMemory (pro Entity), SystemBenchmark (Cross-Tenant), und SystemModelParam (gelernte Gewichte). Die Aggregation laeuft ueber SystemAggregationJob.
A/B Testing fuer AI-Modelle¶
SystemModelParam bekommt zwei zusaetzliche Felder:
experimentId String? // "bank_matching_v2_test"
isControl Boolean @default(false) // Control vs. Variant
AgentOutcome bekommt:
modelVersion Int? // Welche Version des Modells hat entschieden?
experimentId String? // Welches Experiment laeuft?
So koennen zwei Modellversionen parallel laufen und gemessen werden.
Cross-Tenant Privacy (erweitert)¶
k-Anonymitaet (k≥5) ist notwendig aber nicht hinreichend. Zusaetzlich:
- Differential Privacy — Laplace-Noise auf Aggregatwerte (Phase D)
- l-Diversity — Mindestens l verschiedene Werte pro sensitives Attribut in jeder Kohorte
- Dynamisches Kohorten-Merging — Wenn eine Kohorte (z.B. "Villen in Flensburg, GROSS") k<5 hat, wird sie in die naechsthoehere Kohorte gemergt ("Villen in Schleswig-Holstein")
- Benchmarks sind IMMER retrospektiv (min. 1 Quartal alt) und deskriptiv (nie preskriptiv) — Kartellrecht (RealPage-Praezedenz)
DSGVO-Compliance (Architekturentscheidungen)¶
Verarbeitungsverzeichnis (Art. 30 DSGVO)¶
| Verarbeitungstaetigkeit | Rechtsgrundlage | Betroffene | Loeschfrist |
|---|---|---|---|
| Mieterverwaltung (User, Lease, Payment) | Art. 6 (1) b — Vertragserfuellung | Mieter | 10 Jahre nach Vertragsende (§257 HGB) |
| Aenderungsprotokollierung (ChangeLog) | Art. 6 (1) f — berechtigtes Interesse | Mieter, Eigentuemer | Wie Stammdaten |
| KI-Optimierung (EntityMemory, AgentFeedback) | Art. 6 (1) f — berechtigtes Interesse | Mieter (indirekt) | Bei Mandanten-Loeschung |
| Cross-Tenant Benchmarks (SystemBenchmark) | Nicht anwendbar (anonymisiert) | Keine | Unbegrenzt |
| Banking (BankTransaction, Payment) | Art. 6 (1) b + c — Vertrag + gesetzlich | Mieter, Eigentuemer | 10 Jahre (§257 HGB) |
AVV Cross-Tenant-Klausel (Pflicht im Auftragsverarbeitungsvertrag)¶
"Anonymisierte, aggregierte Daten werden mandantenuebergreifend fuer Benchmarks und KI-Optimierung genutzt. Die Anonymisierung erfolgt durch k-Anonymitaet (k≥5), Differential Privacy und dynamisches Kohorten-Merging. Eine Re-Identifizierung einzelner Mandanten oder Personen ist ausgeschlossen."
Interessenabwaegung fuer KI-Features (Art. 6 (1) f)¶
Muss dokumentiert werden fuer: EntityMemory, EntityInsight, AgentFeedback, ChangeLog wo containsPII=true. Die Abwaegung, einschliesslich Argumentation und Ergebnis, muss schriftlich dokumentiert werden (Art. 5 Abs. 2 DSGVO Rechenschaftspflicht).
EU Data Act (seit 12.09.2025 verbindlich)¶
- Mandanten muessen ihre Daten exportieren und zu einem Wettbewerber mitnehmen koennen
- Das DataExportRequest-Model ist ein Anfang, aber es muss ein Self-Service-Datenexport im Produkt existieren — nicht nur auf Anfrage
- Gesetzliche Pflicht seit September 2025 — muss vor Go-Live umgesetzt sein
Datenschutz-Folgenabschaetzung (DPIA — Art. 35, PFLICHT vor Go-Live)¶
Model DataProtectionImpactAssessment {
id String @id @default(cuid())
processingName String // "Cross-Tenant Benchmarking", "AI Bank Matching"
description String
necessity String // Warum ist die Verarbeitung erforderlich?
risks Json // [{risk: "Re-Identifizierung", likelihood: "LOW", impact: "HIGH"}]
mitigations Json // [{measure: "k-Anonymitaet k≥5", status: "IMPLEMENTED"}]
residualRisk String // "LOW", "MEDIUM", "HIGH"
approvedBy String? // Datenschutzbeauftragter
approvedAt DateTime?
reviewDate DateTime // Naechste Ueberpruefung
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
EU AI Act — Risiko-Klassifizierung¶
| AI-Feature | Risiko-Klasse | Anforderung | Status |
|---|---|---|---|
| Ticket-KI (Kategorisierung, Routing) | Minimal | Transparenzpflicht | ✅ Machbar |
| Bank-Matching-KI | Begrenzt | Transparenz + Logging | ⚠️ AgentOutcome reicht |
| Mietanpassungs-Agent | Begrenzt/Hoch | Human-in-the-Loop + Logging | ⚠️ requiresHumanApproval noetig |
| Xolib Score (extern nutzbar) | Potenziell Hoch | Konformitaetsbewertung, Audit | ❌ Nicht adressiert |
| Cross-Tenant Benchmarks | Minimal | Keine besonderen Anforderungen | ✅ |
OpenAI als Subprocessor¶
- OpenAI muss als Subprocessor im AVV gelistet sein
- PII-Stripping-Middleware PFLICHT (Prinzip 14): Mieternamen → Pseudonyme, IBANs → maskiert
- Zuordnung (Pseudonym → Realdaten) bleibt lokal bei Xolib
- Bestehende Implementierung:
pii-mask.ts— muss auf Vollstaendigkeit geprueft werden
DataExportRequest (erweitert fuer Art. 20 + EU Data Act)¶
Neue Felder:
exportScope String // "TENANT_FULL", "TENANT_FINANCIAL", "USER_PERSONAL", "PROPERTY_SINGLE"
includeHistory Boolean @default(false) // Inkl. ChangeLog-Eintraege?
includeAI Boolean @default(false) // Inkl. EntityMemory, Insights?
DSGVO-Risikomatrix¶
| Architektur-Element | Risiko | Status | Massnahme |
|---|---|---|---|
| ChangeLog | Mittel | ⚠️ | containsPII + DataRetentionPolicy koppeln |
| SystemBenchmark | Niedrig | ✅ | Anonymisierung wie geplant |
| EntityMemory | Hoch | ⚠️ | containsPII + Interessenabwaegung dokumentieren |
| EntityInsight | Mittel | ⚠️ | containsPII + Loeschkonzept |
| H3 Spatial | Kein Risiko | ✅ | Keine Massnahmen noetig |
| Webhooks/API | Mittel | ⚠️ | Subprocessor-Dokumentation, IntegrationLog Retention |
| OpenImmo | Niedrig | ✅ | Kontaktdaten im Export beachten |
| External Data | Kein Risiko | ✅ | Alles oeffentliche Daten |
| Datenexport (EU Data Act) | Hoch | ❌ | Self-Service-Export vor Go-Live |
| Art. 22 Human-in-the-Loop | Kritisch | ❌ | requiresHumanApproval auf AgentOutcome |
| EU AI Act Xolib Score | Hoch | ❌ | Konformitaetsbewertung wenn extern nutzbar |
| DPIA (Art. 35) | Pflicht | ❌ | 3 DPIAs vor Go-Live |
| OpenAI Subprocessor | Hoch | ⚠️ | PII-Stripping (pii-mask.ts) pruefen + AVV |
API-Versionierung (Architekturentscheidung)¶
- URL-basierte Versionierung:
/api/v1/,/api/v2/ - ApiClient bekommt:
apiVersion String @default("v1") - Deprecation-Policy: Alte API-Version mindestens 12 Monate nach Release der neuen verfuegbar
- IntegrationLog trackt Version ueber den Endpoint-Pfad
Observability (Architekturentscheidung)¶
Monitoring-Daten gehoeren NICHT in die Anwendungs-DB — sie werden in spezialisierten Systemen gespeichert:
- Agent-Performance (Latenz, Fehlerrate, Feedback-Score) → AgentOutcome + Dashboard-View
- System-Metriken (API-Latenz, DB-Query-Time, Queue-Depth) → Extern (Sentry, Grafana)
- Tenant-Nutzungsmetriken (Logins/Tag, API-Calls/Monat, Features used) → Aggregation auf IntegrationLog oder leichtgewichtiges TenantUsageMetric
- Alerting: Wenn Agents langsamer werden oder haeufiger Fehler machen → sofort sichtbar
Zusammenfassung¶
| Bereich | Status | Handlungsbedarf |
|---|---|---|
| Tenant/User | 6/10 | User splitten, PII verschluesseln |
| Property/Unit | 5/10 | Property splitten, Unit bereinigen, Equipment normalisieren |
| Lease/Payment | 8/10 | RentAdjustment fehlt, matchCorrected fehlt |
| Historisierung | 0/10 | ChangeLog SOFORT bauen |
| Cross-Tenant | 3/10 | Event Sourcing da, Benchmarks fehlen |
| Tickets/Komm. | 8/10 | Nummern-Umbau, Translation bereinigen |
| BK/HK | 9/10 | costType Enum |
| Handwerker | 8/10 | trade Enum |
| WEG | 9/10 | Keine Aenderungen |
| Gewerbe | 8/10 | Nebenkosten normalisieren |
| Score | 10/10 | Benchmark-Extension |
| Banking | 7/10 | Verschluesselung, matchCorrected |
| GDPR | 5/10 | RetentionPolicy, ExportRequest |
| SaaS/Billing | 7/10 | Plan.features normalisieren |
| AI/Events | 9/10 | EntityMemory (Phase C) |
| Spatial Intelligence | 0/10 | H3 auf Property (2 Felder, Phase B+) |
| Ecosystem/Integrations | 0/10 | ApiClient, Webhooks (Phase D+) |
| OpenImmo | 0/10 | Mapping-Model + ImportJob (Phase C+) |
| External Data | 0/10 | ExternalDataSource (Phase F+) |
| Entity Insights | 0/10 | EntityInsight (Phase F+) |
Gesamtbewertung aktuell: 7/10 Zielbewertung nach Umbau: 9.5/10
Domain 17: SPATIAL INTELLIGENCE (NEU)¶
H3 Geospatial Index auf Property¶
Zwei Felder auf Property — kostet fast nichts, ermoeglicht alles Spaetere:
h3IndexRes8 String? // "881f1d4883fffff" — Stadtteil (~460m)
h3IndexRes9 String? // "891f1d4883bffff" — Block (~175m)
@@index([h3IndexRes8])
@@index([h3IndexRes9])
Ermoeglicht:
- Nachbarschafts-Benchmarks ohne teure Spatial Joins
- "Was kosten Betriebskosten im gleichen Block?" in Millisekunden
- Cross-Tenant-Aggregation auf raeumlicher Ebene (neues cohortH3Res8 auf SystemBenchmark)
- Spaeter: Heatmaps fuer Mietpreisentwicklung, Leerstandsprognose, Sanierungswellen
- Prioritaet: MIT Phase B (2 Felder hinzufuegen kostet nichts)
Domain 18: ECOSYSTEM & INTEGRATIONS (NEU — Platform-Architektur)¶
ApiClient (NEU)¶
Model ApiClient {
id String @id @default(cuid())
tenantId String
name String // "DATEV Export", "ImmoScout Sync"
clientId String @unique
clientSecret String // encrypted
scopes String[] // ["properties:read", "payments:read"]
webhookUrl String?
isActive Boolean @default(true)
rateLimitTier String // "STANDARD", "PREMIUM", "PARTNER"
createdAt DateTime @default(now())
}
WebhookSubscription (NEU)¶
Model WebhookSubscription {
id String @id @default(cuid())
tenantId String
apiClientId String
eventType String // "property.updated", "payment.received"
targetUrl String
secret String // HMAC signature
isActive Boolean @default(true)
failCount Int @default(0)
maxRetries Int @default(5)
retryBackoffMs Int @default(60000) // Exponential backoff
lastTriggeredAt DateTime?
lastFailedAt DateTime?
lastErrorCode Int?
isSuspended Boolean @default(false) // Auto-suspend nach maxRetries
}
WebhookDeliveryLog (NEU — Dead Letter Queue)¶
Model WebhookDeliveryLog {
id String @id @default(cuid())
subscriptionId String
eventType String
payload Json
attempt Int @default(1)
statusCode Int?
responseTimeMs Int?
success Boolean
createdAt DateTime @default(now())
@@index([subscriptionId, createdAt])
}
IntegrationLog (NEU)¶
Model IntegrationLog {
id String @id @default(cuid())
tenantId String
apiClientId String
endpoint String
method String
statusCode Int
latencyMs Int
createdAt DateTime @default(now())
@@index([tenantId, apiClientId, createdAt])
}
- Warum Moat: Sobald DATEV, ImmoScout24, Bank und Handwerkerportal ueber Webhooks synchronisiert sind, wird Xolib zum Nervensystem des Betriebs
- Prioritaet: MIT Phase D (Grundstein fuer Oekosystem)
Domain 19: OPENIMMO (NEU — Daten-Ingestion)¶
OpenImmoMapping (NEU)¶
Model OpenImmoMapping {
id String @id @default(cuid())
tenantId String
xolibEntityType String // "Property", "Unit", "Lease"
xolibField String // "sqm", "baseRent", "heatingType"
openImmoPath String // "flaechen.wohnflaeche", "preise.nettokaltmiete"
transformRule String? // "DIRECT", "ENUM_MAP", "CUSTOM"
customMapping Json?
@@unique([tenantId, xolibEntityType, xolibField])
}
- Zweck: Mandantenspezifische Feldmappings — verschiedene Portale erwarten verschiedene OpenImmo-Felder
- Switching Cost: Jedes konfigurierte Mapping ist ein weiterer Wechselkostenpunkt
- Prioritaet: MIT Phase C (Onboarding-Turbo, Daten-Ingestion)
ImportJob (NEU — Onboarding Massen-Import)¶
Model ImportJob {
id String @id @default(cuid())
tenantId String
sourceSystem String // "DOMUS", "AAREON", "EXCEL", "OPENIMMO", "CSV"
status String // "UPLOADED", "VALIDATING", "IMPORTING", "DONE", "FAILED"
totalRows Int?
importedRows Int? @default(0)
errorRows Int? @default(0)
errorLog Json? // [{row: 42, field: "zip", error: "invalid"}]
fileUrl String?
startedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
@@index([tenantId, status])
}
- Zweck: Massen-Import beim Wechsel von DOMUS/Aareon/Excel — kritischster Moment im Kundenlebenszyklus
- ChangeLog-Integration: Jeder Import-Datensatz bekommt
source: "IMPORT"— KI weiss welche Daten importiert vs. organisch entstanden sind - Prioritaet: MIT Phase C+
Domain 20: EXTERNAL DATA ENRICHMENT (NEU)¶
ExternalDataSource (NEU)¶
Model ExternalDataSource {
id String @id @default(cuid())
sourceKey String @unique // "boris_bodenrichtwert", "mietspiegel_berlin"
sourceType String // "API", "SCRAPE", "MANUAL", "OPENDATA"
baseUrl String?
lastSyncAt DateTime?
syncInterval String? // "DAILY", "WEEKLY", "QUARTERLY", "YEARLY"
dataVersion String?
isActive Boolean @default(true)
}
ExternalDataPoint (NEU)¶
Model ExternalDataPoint {
id String @id @default(cuid())
sourceId String
h3IndexRes8 String // Spatial Reference — verbindet externe Daten mit Properties
dataKey String // "bodenrichtwert_eur_sqm", "laerm_db_tag"
value String // JSON stringified
validFrom DateTime
validUntil DateTime?
confidence Float? @default(1.0) // 0.0-1.0 — API_LIVE=1.0, SCRAPED=0.7, MANUAL=0.5
sourceMethod String? // "API_LIVE", "API_CACHED", "MANUAL", "SCRAPED"
createdAt DateTime @default(now())
@@index([h3IndexRes8, dataKey])
@@index([sourceId, dataKey])
}
Oeffentliche Datenquellen: - Bodenrichtwerte (BORIS-D, Gutachterausschuesse) - Mietspiegel (kommunale Veroeffentlichungen) - Grundsteuerdaten (BORIS) - Energieausweise (DENA-Datenbank) - Demografische Daten (Destatis, Zensus) - OEPNV-Anbindung (GTFS-Feeds) - Laermkarten, Hochwasserrisiko (Umweltbundesamt)
- Warum H3 hier kritisch ist:
h3IndexRes8verbindet externe Daten mit internen Properties. Bodenrichtwerte, Mietspiegel und Laermkarten koennen alle auf H3-Zellen gemappt und mit Property-Daten geJOINed werden - Prioritaet: Phase F erweitern
Domain 21: ENTITY INSIGHTS (NEU — Relationale KI-Erkenntnisse)¶
EntityInsight (NEU)¶
Model EntityInsight {
id String @id @default(cuid())
tenantId String? // NULL = Cross-Tenant Insight
sourceType String // "ServiceProvider"
sourceId String?
targetType String // "Ticket"
targetId String?
insightType String // "CORRELATION", "ANOMALY", "PREDICTION", "RECOMMENDATION"
insightKey String // "follow_up_ticket_rate"
value Json // { rate: 0.4, benchmark: 0.15, confidence: 0.87 }
containsPII Boolean @default(false) // DSGVO: true wenn personenbezogen
isActive Boolean @default(true)
hitCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([sourceType, insightKey])
@@index([tenantId, insightType])
}
- Zweck: Bruecke zwischen EntityMemory (pro Entity) und SystemBenchmark (Aggregat) — speichert relationale Insights die keine der beiden abdeckt
- Beispiel: "Handwerkerfirma X: 40% hoehere Folgetickets als Firma Y" — existiert implizit in den Daten, aber kein Model speichert solche Cross-Entity-Erkenntnisse
- Prioritaet: Phase F erweitern
Xolib Flywheel (Architekturprinzip)¶
Mandant nutzt Xolib fuer Verwaltung
↓
Daten fliessen in ChangeLog + XolibEvent
↓
EntityMemory komprimiert Kontext pro Objekt/Mieter
↓
Cross-Tenant Benchmarks + External Data Enrichment
↓
KI wird besser (Matching, Scoring, Predictions)
↓
Mandant sieht "Ihr Wert vs. Markt" + KI-Empfehlungen
↓
Mandant bindet DATEV/ImmoScout/Bank ueber API/Webhooks
↓
Wechselkosten steigen, Retention steigt
↓
Mehr Mandanten → mehr Cross-Tenant Intelligence
↓
Netzwerkeffekt: Jeder neue Mandant macht das System schlauer
↓
Branchenstandard: Drittanbieter bauen auf Xolib-API
Erweiterte Migrations-Reihenfolge¶
| Phase | Was | Wann |
|---|---|---|
| A | ChangeLog (Historisierung) | SOFORT |
| B | Property/Unit Split | JETZT |
| B+ | H3 Index auf Property (2 Felder) | MIT Phase B |
| C | Cross-Tenant + EntityMemory | Naechster Sprint |
| C+ | OpenImmo Import/Export | MIT Phase C |
| D | Sicherheit + DSGVO | Vor Go-Live |
| D+ | ApiClient + Webhooks (Oekosystem-Grundstein) | MIT Phase D |
| E | User Split | Bei Personen V6 |
| F | Extended Intelligence | Nach Go-Live |
| F+ | ExternalDataSource + EntityInsight | Phase F erweitern |
Gesamtbewertung aktuell: 7/10 Zielbewertung nach Umbau: 9.5/10