wiki/b-reference/platform/database-architecture.md

16 KiB

Datenbank-Architektur

Ueberblick

PowerOn verwendet PostgreSQL mit einer klaren Trennung: jedes Modul (App, Chat, Billing, Knowledge, Features) hat eine eigene physische Datenbank (poweron_*). Datenbank-Zugriffe laufen ueber Interfaces (interfaceDb*.py), die auf dem zentralen DatabaseConnector (connectorDbPostgre.py) aufbauen. Datenbanken und Tabellen werden automatisch beim ersten Zugriff erzeugt -- kein manuelles Schema-Management noetig.


Architektur-Stack

Route / Service
    |
    v
Interface (interfaceDb*.py)
    |-- XxxObjects Klasse
    |-- getInterface() Factory (cached)
    |-- setUserContext() -> RBAC-Anbindung
    |
    v
DatabaseConnector (connectorDbPostgre.py)
    |-- Auto-Create Database
    |-- Auto-Create/Extend Tables (aus Pydantic-Modellen)
    |-- CRUD-Operationen (getRecordset, recordCreate, recordUpdate, recordDelete)
    |
    v
PostgreSQL (+ pgvector Extension)

Datenbanken

Inventar

Datenbank Modul / Zweck Interface
poweron_app Kernapplikation: UAM, Mandanten, Rollen, RBAC-Regeln, Navigation, Config interfaceDbApp.py
poweron_chat Chat-Verlaeufe, Messages, Attachments interfaceDbChat.py
poweron_management System-Management, Logs interfaceDbManagement.py
poweron_knowledge RAG Knowledge-Store (mit pgvector) interfaceDbKnowledge.py
poweron_billing Billing Accounts, Transactions, Usage interfaceDbBilling.py
poweron_billing Subscriptions (gleiche DB wie Billing) interfaceDbSubscription.py
poweron_workspace AI Workspace Feature-Daten Feature-Interface
poweron_automation Automation v1 Workflows, Runs Feature-Interface
poweron_automation2 Automation v2 (Graph-Editor) Workflows, Runs Feature-Interface
poweron_trustee Trustee Feature-Daten Feature-Interface
poweron_commcoach CommCoach Feature-Daten Feature-Interface
poweron_neutralization Neutralisierungs-Daten Feature-Interface
poweron_realestate RealEstate Feature-Daten Feature-Interface
poweron_teamsbot Teams-Bot Feature-Daten Feature-Interface
poweron_test Test-Datenbank Nur in Tests

Namenskonvention

Feature-Datenbanken folgen dem Pattern poweron_{featureCode.lower()}. Der Datenbankname wird hart-codiert in der _initializeDatabase() Methode des jeweiligen Interfaces.

Konfiguration

Verbindungsparameter kommen aus APP_CONFIG (config.ini / Environment):

Key Beschreibung
DB_HOST PostgreSQL Host
DB_PORT PostgreSQL Port
DB_USER Datenbank-Benutzer
DB_PASSWORD_SECRET Passwort (ggf. verschluesselt)
DB_DATABASE Default-DB-Name (Standard: poweron_app)

Interface-Pattern

Jedes Interface folgt einem einheitlichen Aufbau:

XxxObjects Klasse

class AppObjects:
    db: DatabaseConnector        # Connector zur eigenen DB
    rbac: RbacClass              # RBAC-Integration
    currentUser: dict            # Aktueller Benutzer
    mandateId: str               # Aktueller Mandant
    featureInstanceId: str       # (nur bei Feature-Interfaces)

_initializeDatabase()

Setzt den Datenbanknamen und erstellt den Connector:

def _initializeDatabase(self):
    dbDatabase = "poweron_app"   # hart-codiert pro Interface
    self.db = DatabaseConnector(
        host=APP_CONFIG.get("DB_HOST"),
        database=dbDatabase,
        user=APP_CONFIG.get("DB_USER"),
        password=APP_CONFIG.get("DB_PASSWORD_SECRET"),
        port=APP_CONFIG.get("DB_PORT")
    )

setUserContext()

Bindet Benutzer und RBAC an die Instanz:

def setUserContext(self, currentUser, mandateId):
    self.currentUser = currentUser
    self.userId = currentUser["id"]
    self.mandateId = mandateId
    self.rbac = RbacClass(self.db, dbApp=dbApp)
    self.db.updateContext(self.userId)

RBAC-Besonderheit: poweron_app ist Sonderfall (dbApp=self.db), weil RBAC-Regeln selbst in poweron_app liegen. Alle anderen Interfaces nutzen getRootDbAppConnector() um Rules aus der App-DB zu lesen:

dbApp = getRootDbAppConnector()
self.rbac = RbacClass(self.db, dbApp=dbApp)

getInterface() Factory

Cached pro Kontext:

Interface Cache-Key Ergebnis
App {mandateId}_{userId} AppObjects
Chat {mandateId}_{featureInstanceId}_{userId} ChatObjects
Billing {userId}_{mandateId} BillingObjects
Knowledge "default" (Singleton) KnowledgeObjects

DatabaseConnector

Auto-Initialisierung

Beim Erstellen eines DatabaseConnector (initDbSystem()):

1. _create_database_if_not_exists()
   -> Verbindet sich zu DB "postgres"
   -> Prueft pg_database auf Existenz
   -> CREATE DATABASE "{name}" falls fehlend

2. _create_tables()
   -> Erstellt nur die _system Registry-Tabelle
   -> Applikations-Tabellen werden lazy durch Interfaces erzeugt

3. _connect()
   -> psycopg2-Verbindung zur Ziel-DB

4. _initializeSystemTable()
   -> _ensureTableExists(SystemTable)

Lazy Table Creation

Tabellen werden bei erstem Zugriff automatisch aus Pydantic-Modellen erzeugt (_ensureTableExists(model_class)):

  • Tabellenname = Python-Klassenname des Pydantic-Modells (z.B. Role, AccessRule, UserMandate)
  • Spalten werden aus Modellfeldern abgeleitet
  • JSONB fuer komplexe Typen (dict, list, Pydantic-Submodelle)
  • vector fuer pgvector-Felder (Knowledge-Store)
  • Additive Migration: Wenn die Tabelle existiert, werden fehlende Spalten automatisch hinzugefuegt. Bestehende Spalten werden nicht geaendert oder entfernt.

Connector-Caching

_get_cached_connector(host, database, port)

Wiederverwendet einen DatabaseConnector pro Datenbank-Identitaet (host:database:port). Genutzt von App, Management, Knowledge. Chat und Billing instanziieren direkt (kein Cache).

userId Context

userId wird ueber eine contextvar am Connector gesetzt (updateContext(userId)). Damit werden System-Felder (_createdBy, _updatedBy) automatisch befuellt.


RBAC-Integration in DB-Abfragen

Die RBAC-Schicht greift direkt in Datenbank-Abfragen ein:

Kern-Funktionen in interfaceRbac.py

Funktion Zweck
getRecordsetWithRBAC() Listet Datensaetze mit RBAC-Filter in SQL WHERE
buildRbacWhereClause() Uebersetzt Access Levels in SQL-Bedingungen
buildDataObjectKey() Baut Keys wie data.uam.UserInDB fuer Rule-Lookup
TABLE_NAMESPACE Mapping von Modellnamen zu logischen Namespaces

Namespace-Mapping

TABLE_NAMESPACE ordnet Pydantic-Modellnamen logischen Bereichen zu:

Namespace Modelle (Beispiele)
uam UserInDB, UserMandate, Role, AccessRule
chat ChatHistory, ChatMessage
files FileRecord, FileFolder
feature.trustee TrusteePosition, TrusteeDocument
feature.workspace Workspace-spezifische Modelle

SQL-Filter nach Access Level

Level SQL WHERE
a (ALL) Kein Filter
m (MANDATE) mandateId = :mandateId
o (OWN) _createdBy = :userId
n (NONE) 1=0 (kein Ergebnis)

System-Felder

Jedes PowerOnModel erbt automatisch System-Felder:

Feld Beschreibung Schutz
id UUID, auto-generiert Immutable
_createdBy userId des Erstellers Immutable
_createdAt Zeitstempel Erstellung Immutable
_updatedBy userId der letzten Aenderung Auto-gesetzt
_updatedAt Zeitstempel letzte Aenderung Auto-gesetzt

Felder mit fuehrendem _ sind fuer Anwendungs-CUD geschuetzt -- der Connector erzwingt das unabhaengig von Access Rules.


Feature-DB-Registrierung

Jedes Interface registriert seine Datenbank ueber registerDatabase() aus dbRegistry.py:

  1. Jedes interfaceDb*.py / interfaceFeature*.py definiert eine Modul-Konstante (z.B. appDatabase = "poweron_app")
  2. Auf Modul-Ebene wird registerDatabase(appDatabase) aufgerufen — damit ist die DB im zentralen Registry
  3. Der DatabaseConnector erzeugt die DB automatisch beim ersten Zugriff
  4. Neue DBs werden automatisch erkannt, entfernte DBs verschwinden

Database Health und Orphan-Scanner

Ueberblick

SysAdmin-Seite unter Admin > System > Datenbank-Gesundheit mit zwei Funktionen:

  1. Tabellenstatistiken — Row Count, Size, Index Size, Last Vacuum/Analyze fuer alle Tabellen
  2. Orphan Cleanup — Generische Erkennung verwaister Datensaetze mit Clean-Buttons

Dynamische DB-Registry (modules/shared/dbRegistry.py)

  • registerDatabase(dbName, configPrefix) — oeffentliche API, aufgerufen in jedem Interface
  • _getRegisteredDatabases() — alle registrierten DBs
  • _getConnectorForDb(dbName) — Factory fuer read-only Connector

Model-Registry (modules/datamodels/datamodelBase.py)

  • _MODEL_REGISTRY: Dict[str, Type[PowerOnModel]] — automatisch via __init_subclass__
  • Jede PowerOnModel-Subklasse registriert sich beim Import (Tabellenname = Klassenname)

FK-Discovery (modules/shared/fkRegistry.py)

  • Scannt alle PowerOnModel-Subklassen nach fk_target in json_schema_extra
  • Baut automatisch {tableName → dbName} Mapping aus den Annotationen
  • Fallback: Catalog-Query (information_schema.tables) fuer unmapped Tables
  • Cached nach erstem Scan
  • FkRelationship Dataclass: sourceDb, sourceTable, sourceColumn, targetDb, targetTable, targetColumn

FK-Annotationen (fk_target)

Jedes *Id-Feld das eine echte FK-Beziehung darstellt, hat fk_target in json_schema_extra mit drei Pflichtschluesseln (db, table, labelField):

mandateId: str = Field(
    ...,
    json_schema_extra={
        ...,
        "fk_target": {"db": "poweron_app", "table": "Mandate", "labelField": "label"},
    },
)
  • Standard targetColumn ist "id", Sonderfall Feature.code nutzt "column": "code"
  • labelField gibt das Feld fuer das menschenlesbare Label an; None = kein Label (Junction-IDs)
  • fk_target ist die einzige FK-Annotation — sowohl fuer Orphan-Detection als auch Label-Resolution und UI-Attribute
  • Felder ohne DB-FK (Stripe-IDs, Graph-Node-IDs, polymorphe referenceId) haben kein fk_target
  • Startup-Validierung (validateFkTargets) prueft Vollstaendigkeit; fehlende Keys brechen den Start ab
  • Soft FK ("softFk": True): Optionaler Marker fuer "weiche" Referenzen, die Sentinel-/Lineage-Werte halten, fuer die absichtlich keine DB-Zeile existiert (z. B. AutoWorkflow.templateSourceId = "trustee-receipt-import" aus featureModule.getTemplateWorkflows()). Label-Resolution funktioniert weiterhin; der Orphan-Scanner ueberspringt soft FKs vollstaendig (kein Display, kein Cleanup), damit korrekte Datensaetze nicht geloescht werden.

Orphan-Scanner (modules/system/databaseHealth.py)

  • _getTableStats(dbFilter)pg_stat_user_tables + pg_total_relation_size
  • _scanOrphans(dbFilter) — Same-DB: NOT EXISTS, Cross-DB: Parent-IDs laden + NOT IN
  • _cleanOrphans(db, table, column) — loescht Orphans, gibt Count zurueck
  • _cleanAllOrphans(force, excludeUserFks) — alle Orphans bereinigen; excludeUserFks=True ueberspringt UserInDB.id-Referenzen
  • _isUserIdFk(targetTable, targetColumn) — Helper: matcht UserInDB.id (case-insensitive). Eine Stelle, die scan-route + clean-all + frontend gemeinsam nutzen
  • 5-Minuten-Cache fuer Orphan-Ergebnisse
  • Pre-Filter: Source-/Target-Tabelle muss existieren, Source-/Target-Spalte muss als physische Spalte vorhanden sein, FK darf nicht softFk: True sein. Erst dann wird ein Orphan-Eintrag erzeugt — sonst werden produktive Datensaetze geloescht (z. B. Trustee-Workflows mit Sentinel-templateSourceId).
  • User-FK-Filter: Orphans, die auf UserInDB.id zeigen (Audit-/Billing-/Membership-Reste geloeschter User), gehoeren in den separaten User-Purge-Workflow. Frontend-Checkbox Ohne FK-Referenzen zu UserInDB.id (default ON) blendet sie aus dem Scan-Resultat aus, und clean-all ueberspringt sie identisch — die UI zeigt also nie Orphans an, die der naechste Klick nicht auch loeschen wuerde.

API (modules/routes/routeAdminDatabaseHealth.py)

Methode Pfad Beschreibung
GET /api/admin/database-health/stats Tabellenstatistiken (optional ?db=...)
GET /api/admin/database-health/orphans Orphan-Scan (optional ?db=..., ?excludeUserFks=true)
POST /api/admin/database-health/orphans/clean Einzeln-Cleanup {"db","table","column"}
POST /api/admin/database-health/orphans/clean-all Batch-Cleanup {"force","excludeUserFks"}

Alle Endpunkte: SysAdmin-only via requireSysAdminRole.

Migration API (Streaming, seit 2026-05-28)

Methode Pfad Beschreibung
GET /api/admin/database-health/migration/export-stream Streaming-Export aller/ausgewaehlter DBs als JSON via StreamingResponse. Query-Params: databases (kommagetrennt, optional), filename. Frontend nutzt ReadableStream + File System Access API fuer direktes Streaming-to-Disk.
GET /api/admin/database-health/migration/export-single?database=... Export einer einzelnen DB als JSON.
POST /api/admin/database-health/migration/upload-import Multipart-Upload der Import-JSON auf Disk. Gibt {token, fileSizeMb} zurueck.
GET /api/admin/database-health/migration/process-import-stream?token=... Streaming-Validierung + Split der hochgeladenen Datei via StreamingResponse (ndjson). Two-Pass mit ijson: Pass 1 validiert + extrahiert Metadaten, Pass 2 splittet in per-Table JSONL. Liefert Progress-Events {phase, db, table, rows}.
POST /api/admin/database-health/migration/import-single Import einer einzelnen DB (aus per-Table JSONL oder Legacy-Format).
POST /api/admin/database-health/migration/import-done Cleanup temporaerer Dateien nach abgeschlossenem Import.

Frontend (AdminDatabaseHealthPage.tsx)

  • Tab "Statistiken": Sortierbare Tabelle mit DB-Filter und Summary-Leiste
  • Tab "Orphan Cleanup": Tabelle mit Clean-Button pro Zeile + "Alle bereinigen", Checkboxen Nur Probleme und Ohne FK-Referenzen zu UserInDB.id (letzteres default ON)
  • Tab "Migration": Streaming-Export (DB-Auswahl, Fortschrittslog mit DB x/n + MB) und Streaming-Import (Upload, Validierungs-Progress, per-DB-Import mit Modus Neu/Zusammenfuehren)

Schluessel-Dateien

Thema Pfad
PostgreSQL Connector platform-core/modules/connectors/connectorDbPostgre.py
DB-Registry platform-core/modules/shared/dbRegistry.py
FK-Registry platform-core/modules/shared/fkRegistry.py
Database Health platform-core/modules/system/databaseHealth.py
Health API Route platform-core/modules/routes/routeAdminDatabaseHealth.py
App-DB Interface platform-core/modules/interfaces/interfaceDbApp.py
Chat-DB Interface platform-core/modules/interfaces/interfaceDbChat.py
Management-DB Interface platform-core/modules/interfaces/interfaceDbManagement.py
Knowledge-DB Interface platform-core/modules/interfaces/interfaceDbKnowledge.py
Billing-DB Interface platform-core/modules/interfaces/interfaceDbBilling.py
Subscription Interface platform-core/modules/interfaces/interfaceDbSubscription.py
RBAC in DB-Schicht platform-core/modules/interfaces/interfaceRbac.py
Feature-Interface (Template) platform-core/modules/interfaces/interfaceFeatures.py
Bootstrap / DB-Seed platform-core/modules/interfaces/interfaceBootstrap.py
DB-Migration (Streaming) platform-core/modules/system/databaseMigration.py
DB-Migration Script platform-core/scripts/script_db_export_migration.py
Datenmodelle (Pydantic) platform-core/modules/datamodels/
Frontend Health Page ui-nyla/src/pages/admin/AdminDatabaseHealthPage.tsx