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_chatbot |
Chatbot Feature-Daten | 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:
- Jedes
interfaceDb*.py/interfaceFeature*.pydefiniert eine Modul-Konstante (z.B.appDatabase = "poweron_app") - Auf Modul-Ebene wird
registerDatabase(appDatabase)aufgerufen — damit ist die DB im zentralen Registry - Der
DatabaseConnectorerzeugt die DB automatisch beim ersten Zugriff - Neue DBs werden automatisch erkannt, entfernte DBs verschwinden
Database Health und Orphan-Scanner
Ueberblick
SysAdmin-Seite unter Admin > System > Datenbank-Gesundheit mit zwei Funktionen:
- Tabellenstatistiken — Row Count, Size, Index Size, Last Vacuum/Analyze fuer alle Tabellen
- 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_targetinjson_schema_extra - Baut automatisch
{tableName → dbName}Mapping aus den Annotationen - Fallback: Catalog-Query (
information_schema.tables) fuer unmapped Tables - Cached nach erstem Scan
FkRelationshipDataclass: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
targetColumnist"id", SonderfallFeature.codenutzt"column": "code" labelFieldgibt das Feld fuer das menschenlesbare Label an;None= kein Label (Junction-IDs)fk_targetist 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"ausfeatureModule.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=Trueueberspringt UserInDB.id-Referenzen_isUserIdFk(targetTable, targetColumn)— Helper: matchtUserInDB.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: Truesein. 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.idzeigen (Audit-/Billing-/Membership-Reste geloeschter User), gehoeren in den separaten User-Purge-Workflow. Frontend-CheckboxOhne FK-Referenzen zu UserInDB.id(default ON) blendet sie aus dem Scan-Resultat aus, undclean-allueberspringt 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 ProblemeundOhne 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 |