Datenbank
Die Persistenz läuft über ORM++
(privates Go-Modul, GOPRIVATE=gitlab.techeve.de). Backend per Config:
- SQLite (Default):
database_pathin der config.json — eingebettet, null Betriebsaufwand. - PostgreSQL:
database_dsn: "postgres://..."— sonst nichts. ORM++ garantiert Verhaltensgleichheit; App-Code verzweigt nie nach Backend.
Verbindung, Model-Registrierung, Migration und die ORM++-Worker
(Projektionen, Snapshots, Archivierung) kapselt internal/storage/database.go
(storage.Open). Die vollständige ORM++-API-Referenz liegt im ORM++-Repo
unter doc/API.md.
Mandanten-Modell
Abschnitt betitelt „Mandanten-Modell“Zwei Ebenen, bewusst getrennt:
- Gateway-Konfiguration (User, APIKey, Tenant, SmtpRoute,
BlocklistEntry, OutboundMail):
orm.TenantFree()— globale Tabellen ohne Mandanten-Spalte. - Formulardaten (Submission, Nonce): leben im ORM++-Tenant der
jeweiligen Kundenseite (
Tenant.OrmTenantID, bei Mandanten-Anlage überdb.Tenants().Createerzeugt). Jeder Zugriff brauchtorm.WithTenant(ctx, tenant.OrmTenantID)— fail-closed. Dadurch gibt es DSGVO-Export (Tenants().Export) und Recht-auf-Vergessen (Archive+Purge) pro Kundenseite geschenkt.
Event Sourcing
Abschnitt betitelt „Event Sourcing“Submission ist event-sourced: Statusübergänge (received → scored → sent/quarantined → released/discarded/blocked/failed) sind Events; die
Historie (sub.History(ctx)) ist das Audit-Log des Konzepts. Das Struct
selbst ist nur das gefaltete Read-Model (Apply-Funktion in
internal/core/domain/submission.go).
Achtung: Der Query-Builder kennt auf ES-Read-Models nur die eigenen
Struct-Felder — die Aggregate-Zeitstempel sind Methoden. Deshalb hat
Submission ein eigenes ReceivedAt-Feld (orm:"index").
Feld-Verschlüsselung
Abschnitt betitelt „Feld-Verschlüsselung“Tenant.SigningSecret und SmtpRoute.Password tragen orm:"encrypted"
(AES-256-GCM, Schlüssel = encryption_key aus der config.json). Die DB
sieht nur Ciphertext; die Felder sind nicht filter-/sortierbar und werden
nie serialisiert. Den Key sichern — ohne ihn sind diese Felder verloren.
Schema-Änderungen
Abschnitt betitelt „Schema-Änderungen“- Additiv (neue Spalte/Index/Model): Model anpassen bzw. in
registerModels()registrieren — ORM++ Auto-Diff erledigt den Rest. - Nicht additiv (Feld umbauen/entfernen):
storage.SchemaVersionerhöhen undorm.MigrationTo(db, n, ...)-Schritt ergänzen (Expand/Contract mit Dual-Write, siehe ORM++ doc/API.md §8). Entfallende Felder erst mitorm:"deprecated"markieren. - Drift-Schutz: Model geändert ohne Versions-Erhöhung ⇒ Startfehler.
Repositories & Tests
Abschnitt betitelt „Repositories & Tests“CRUD-Zugriffe kapseln die Repositories (internal/storage/repositories);
orm.ErrNotFound wird via translate() zu repositories.ErrNotFound.
Für Submissions (ES) nutzen die Services orm.New/Load/Query direkt.
Tests öffnen eine Wegwerf-DB im Temp-Verzeichnis:
db, err := storage.Open(ctx, orm.SQLite(filepath.Join(t.TempDir(), "test.db")), make([]byte, 32))Vorlage: internal/core/services/services_test.go.
Stolpersteine
Abschnitt betitelt „Stolpersteine“UpdateSetnie mit Pointer-Werten aufrufen (orm.Set("X", time.Now()), nicht&now) — Pointer umgehen die ORM++-Wert-Kodierung und machen die Zeile beim nächsten Scan unlesbar.- Nonce-Einlösung nutzt den Unique-Constraint als atomare Sperre
(
repositories.NonceRepository.Consume): zweiter Insert derselben Nonce ⇒ErrNonceUsed(Replay).