Zum Inhalt springen

Architektur

Das Techeve Form Gateway kompiliert ein Go-Backend (Fiber v3), ein Svelte-5-Admin-Frontend und das Client-Snippet in ein einziges Binary. Das Frontend wird von Vite nach frontend/dist gebaut und per go:embed eingebettet (frontend/embed.go); das Snippet liefert das Gateway unter /form-gateway.js aus (client/embed.go).

  1. Public API (/v1/challenge, /v1/submit) — für die statischen Kundenseiten, ohne Auth. Sicherheit = Prüf-Pipeline (unten).
  2. Admin-API + UI (/api/v1/... + SPA) — echte Auth (argon2id, JWT, statisches RBAC), verwaltet Mandanten, Routen, Quarantäne, Blocklist.
  3. Hintergrund-Worker (internal/worker) — Versand-Queue (alle 15 s), Nonce-/Queue-Cleanup (stündlich), Reputations-Refresh (täglich). Die ORM++-eigenen Worker (Projektionen, Snapshots, Archivierung) startet storage.Open separat.
HTTP-Request
Router + Middlewares internal/api/router, internal/api/middlewares
│ (JWT/API-Key-Auth, RBAC, Logging, Recover; CORS für /v1)
Controller internal/api/controllers
│ (JSON parsen, validieren, IDs als UUID-Strings via Views)
Service internal/core/services
│ (Business-Logik — kennt kein HTTP;
│ GatewayService orchestriert die Prüf-Pipeline)
Repository / ORM++ internal/storage/repositories (CRUD)
│ orm.New/Load/Query direkt für Event-Sourcing (Submission)
ORM++ SQLite (Default) oder PostgreSQL — config-Schalter

Daneben, bewusst DB- und HTTP-frei:

  • internal/pipeline — die reinen Prüf-Schichten: Challenge-Token (HMAC), Proof-of-Work, Eingabe-Härtung, Spam-Score, Rate-Limiter. Vollständig unit-getestet.
  • internal/reputation — IP-/ASN-Lookup (iptoasn.com), Tor-Exits, gespiegelte Admin-Blocklist (in-memory).
  • internal/mailer — SMTP-Versand (STARTTLS/implicit), Routen-Auflösung (Mandanten-Route → geteilte Default-Route), Retry-Backoff über die DB-Queue.

Regeln:

  • Controller enthalten keine Business-Logik und keine DB-Zugriffe; orm.ID geht nach außen immer als UUID-String (View-Structs).
  • Services importieren niemals fiber.
  • Repositories kapseln CRUD; Fehler werden auf repositories.ErrNotFound normalisiert. Für das event-sourcte Submission-Modell nutzen Services die ORM++-API direkt (orm.New/Load/Query im Tenant-Kontext).
  • Domain-Structs (internal/core/domain) haben keine Abhängigkeiten außer ORM++-Tags/Typen.

POST /v1/submit läuft durch GatewayService.Submit (gateway_service.go) — Reihenfolge „billig zuerst” (Konzept §5):

  1. Rate-Limits in-memory: global → IP → Site-Key
  2. Mandant per Site-Key + Kill-Switch + Origin-Allowlist (serverseitig!)
  3. Challenge-Token: HMAC-Signatur, TTL, Site-/Origin-Bindung, Mindest-Ausfüllzeit
  4. Honeypot (Treffer: Erfolg vortäuschen, intern blocked protokollieren)
  5. IP-/ASN-Reputation (Tor, Blocklist, ASN-Sperren)
  6. Proof-of-Work-Verifikation (ein Hash)
  7. Nonce atomar einlösen (Unique-Insert ⇒ Replay-Schutz)
  8. Eingabe-Härtung (Feld-/Längen-Limits, CRLF-Schutz, Reply-To-Validierung)
  9. E-Mail-Blockmuster gegen Reply-To
  10. Tageskontingent (persistent, zählt pending + sent)
  11. Submission-Aggregat anlegen (received-Event) + Spam-Score
  12. Score ≥ Schwelle ⇒ quarantined (Client sieht trotzdem Erfolg), sonst Versand-Queue

Ablehnungen sind nach außen immer 400 {"ok":false,"reason":"rejected"} — der echte Grund steht nur im Log und (als blocked-Submission) im Admin.

Gateway-Mandanten (Tenant) sind globale Konfigurationszeilen; ihre Formulardaten leben im jeweils eigenen ORM++-Tenant (Tenant.OrmTenantID): Submissions und Nonces sind dadurch hart getrennt, DSGVO-Export und -Purge gibt es pro Kundenseite über die ORM++-Tenant-Registry. Submission ist event-sourced — die Historie (received → scored → sent/quarantined → released/discarded) ist das Audit-Log. Details: database.md.

Immer von innen nach außen (Beispiel-Kette existiert komplett für SmtpRoute):

  1. Domain-Modelinternal/core/domain/xyz.go (ORM++-Tags, siehe database.md)

  2. Registrieren — in internal/storage/database.goregisterModels(); bei nicht-additiven Änderungen SchemaVersion erhöhen

  3. Repositoryinternal/storage/repositories/xyz_repository.go (Vorlage: smtproute_repository.go)

  4. Serviceinternal/core/services/xyz_service.go; eigene Fehler als var ErrXyz = errors.New(...)

  5. Controllerinternal/api/controllers/xyz_controller.go mit View-Struct (Vorlage: route_controller.go)

  6. Route + Permission — Code in domain/rbac.go + Rollenkatalog, Route in router.go:

    xyz := api.Group("/xyz")
    xyz.Get("/", middlewares.RequirePermission(domain.PermXyzRead), ctrl.List)

    Wichtig (Fiber v3): Handler laufen in Angabe-Reihenfolge — Middlewares stehen vor dem Controller-Handler, sonst ist die Route ungeschützt. Regressionstest: TestRBACMiddlewareRunsBeforeHandler.

  7. Frontend-API-Klasse + Page — siehe frontend_api.md (Vorlage: api/routes.js + pages/Routes.svelte)

  8. Tests — Service-Test gegen Temp-SQLite (Vorlage: gateway_service_test.go), Routen-Test in router_test.go

Sonderfall: Logik ohne Datenbank (Berechnungen, Versionsinfo): Repository-Schicht weglassen — Kette nur Controller → Service. Referenz: SystemService/SystemController.

cmd/app/main.go Einstiegspunkt: Config → DB → Seeding → Worker → Server
client/ Client-Snippet (form-gateway.js) + Beispielseite
docs/ Diese Dokumentation
internal/api/ HTTP-Transport (Controller, Middlewares, Router)
internal/core/domain/ Entitäten (ORM++-Models) + RBAC-Katalog
internal/core/services/ Business-Logik (inkl. GatewayService = Pipeline-Orchestrierung)
internal/pipeline/ Reine Prüf-Schichten (Token, PoW, Härtung, Spam, Rate-Limits)
internal/reputation/ IP-/ASN-/Tor-Daten (in-memory, täglicher Refresh)
internal/mailer/ SMTP-Versand + Retry-Queue-Verarbeitung
internal/worker/ Hintergrund-Jobs (Versand, Cleanup, Refresh)
internal/storage/ ORM++-Verbindung, Model-Registrierung, Seeding, Repositories
internal/config/ config.json-Management
frontend/src/api/ API-Service-Klassen (fetch-Abstraktion)
frontend/src/pages/ Eine Svelte-Datei pro Route
frontend/embed.go go:embed des dist-Ordners
agent.md Kontext für KI-Assistenten

Beim Start sucht das Binary eine config.json im Datenverzeichnis (-data <dir> bzw. FORMGW_DATA; Default: Verzeichnis des Binaries). Fehlt sie, wird sie mit sicheren Zufallswerten erzeugt — inklusive JWT-Secret und Feld-Verschlüsselungs-Key (encryption_key, sichern!). Details: security.md.

Danach: ORM++ öffnen (SQLite oder database_dsn = PostgreSQL), Migration + ORM++-Worker, idempotentes Seeding (system- und admin-User; das generierte Admin-Passwort erscheint einmalig in der Konsole), Blocklist in den Reputations-Speicher spiegeln, Gateway-Worker starten, Server binden.

Eine Installation besteht aus drei Dateien: Binary, config.json, gateway.db — die Begleitdateien entstehen beim ersten Start von selbst.

  • Semantic Version: Datei VERSION — pflegt der Release-Bot der CI-Pipeline (Conventional Commits ⇒ nächste Version), nicht von Hand.
  • Build-Nummer: .buildnumber lokal (Makefile bump-build), Pipeline-Nummer in CI.
  • Beide werden per -ldflags -X in internal/version injiziert.

Abfragbar über: ./form-gateway -version, GET /api/v1/system/info, Footer der Web-App und make version.

make build läuft: npm auditvite buildgovulncheckgo build. Schlägt ein Sicherheits-Check fehl, bricht der Build ab. ORM++ ist ein privates Modul: einmalig go env -w GOPRIVATE=gitlab.techeve.de (lokal; CI nutzt den Job-Token).

CGO-frei ⇒ Cross-Compiling ohne Toolchains:

Terminal-Fenster
make build-linux # bin/form-gateway-linux-amd64
make build-linux-arm64 # bin/form-gateway-linux-arm64
make build-windows # bin/form-gateway-windows-amd64.exe
make build-macos # bin/form-gateway-darwin-arm64 (Apple Silicon)
make build-macos-intel # bin/form-gateway-darwin-amd64
make build-all # alle Plattformen

Docker: gehärtetes Alpine-Image (non-root, read-only) mit ./data-Volume — Anleitung: docker.md. Kurzform: make docker-build && docker compose up -d.

systemd (/etc/systemd/system/form-gateway.service):

[Unit]
Description=Techeve Form Gateway
After=network.target
[Service]
User=formgw
WorkingDirectory=/opt/form-gateway
ExecStart=/opt/form-gateway/form-gateway -data /var/lib/form-gateway
Restart=on-failure
[Install]
WantedBy=multi-user.target

Davor gehört ein Reverse-Proxy (nginx) mit TLS; die Public API und das Admin-UI laufen über denselben Port. Das Binary behandelt SIGINT/SIGTERM mit Graceful Shutdown (stoppt auch die Worker sauber).

Betriebs-Voraussetzung Versand: mindestens eine SMTP-Route im Admin anlegen — üblicherweise die geteilte Default-Route über den eigenen Mailserver mit fester, DKIM-/SPF-signierter Absenderdomain (z. B. forms.techeve.de). Der Test-Versand-Button prüft Verbindung + Login.