Architektur · v0.30.0

Wie die Fabrik technisch aufgebaut ist.

Von der UI bis zu den Adaptern, die mit den Coding-CLIs sprechen. Hexagonal pro Modul, lokal lauffähig, ohne Cluster und ohne Service-Mesh. Hier kannst du nachlesen, wo welche Schraube sitzt und warum.

hexagonal · Ports & Adapters 7 Run-Phasen ADR-dokumentiert

Ziel der Architektur

Die Plattform ist eine Control Plane für AI-gestützte Softwareentwicklung. Sie kapselt vier Aufgaben in einer Webanwendung: Projekt-Erfassung, Artefakt-Generierung, Run-Orchestrierung und Quality-Bewertung. Coding-CLIs (Claude, Codex, Gemini, Aider) werden als Subprozesse aufgerufen, deren Output strukturiert in der Datenbank landet und live in der UI sichtbar wird.

Zwei Prinzipien, die in V1 bewusst nicht mit Buzzwords überfrachtet werden:

  • Hexagonal pro Modul. Jedes fachliche Modul (z.B. run, wizard, settings) hat domain, application, web und infrastructure getrennt. Domain hängt von nichts ab.
  • Lokale Ausführung. Ein Spring-Boot-Prozess + Postgres in Docker. Kein Cluster, kein Kafka, kein Service-Mesh.
Browser → Spring MVC + Thymeleaf → Application Services → Domain Model
                                                              ↓
                       PostgreSQL ← Repositories ← Adapter
                       (Claude / Codex / Gemini / Aider / Kimi / local-LLM /
                        Bedrock / Vertex / Azure OpenAI / Mock / Git / FS)

Schichten und Module

Innerhalb des Maven-Moduls app liegen alle fachlichen Module unter io.softwarefabrik.app.<modul>. Pro Modul maximal vier Unterpakete:

  • domain/ — Entities, Value Objects, Repository-Interfaces, fachliche Services. Keine Spring-, JPA- oder Web-Annotationen.
  • application/ — Use-Case-Services. Transaktionsgrenzen (@Transactional).
  • web/ — Spring-MVC-Controller, Thymeleaf-Bindings, DTOs, SSE-Endpoints.
  • infrastructure/ — JPA-Repositories, externe Adapter, Filesystem-Zugriffe, Scheduled Jobs.

Abhängigkeitsregeln

  • domain hängt von nichts ab.
  • application nur von domain.
  • web nur von application, nicht direkt domain.
  • infrastructure implementiert domain-Ports.
  • Kein Quergriff in fremde domain-Pakete.

ArchUnit-Tests pinnen diese Regeln. Wer eine falsche Abhängigkeit reinzieht, färbt CI rot.

Die 30 Slices der v0.30.0-Codebasis

agentauditcommonconductorcontractexecutionexportgitguardrailskennzahlenlicensemandantobservabilitypolicyprojectdefinitionpromptprovenancequalitygatereviewrunsdlcsecretssecuritysettingsskillsteamvalidationwebwizardworkflow

Module im Detail

ModulZweckHauptklassen / Routen
agentAgentenrollen + preferredModelAgentDefinition, AgentRoleService, /agents
auditAppend-only Audit-Log mit signierter Ed25519-HashketteAuditEvent, AttestierungService, /attestierung
commonShared utilitiesID-Generator, Clock-Abstraktion
conductorPlugin-/Skills-Sync, AGENTS.md-Writer (v0.6.0+)PluginCatalog, SkillsCatalog, ConductorWorkspaceWriter
executionRun-Ausführung, Sandbox-Factory, Stream-ParserExecutionAdapterRegistry, LocalProcessSandbox + ContainerProcessSandbox (v0.7.0), ExecutionSandboxFactory, ClaudeStreamJsonParser, TokenEstimator
gitGit im Workspace, Inline-Diff vor ApprovalGitService.diffSeit() (v0.8.0), shell-out zu git
licenseTier-Lizenzen, Lease-JWT/license, /admin/license
observabilityPrometheus-Metriken, Observability Ebene 1 (seit v0.9.0)ObservabilityConfig, Micrometer-Registry unter /actuator/prometheus (ROLE_ADMIN)
policyApproval-Policies, Policy-as-Code, Compliance-ProfileApprovalDecision, PolicyDocument, /policy-as-code, /compliance-profile
projectdefinitionProjekt-Editor/projects, /projects/{id}/edit
promptMarkdown-Artefakt-ErzeugungTemplates unter resources/prompts/
qualitygateVerdict-Aggregation/runs/{id}/quality-gate
reviewSechs read-only Revieweraider-review, claude-review, security-reviewer, architecture-reviewer, hallucination-reviewer, dependency-scan
runRun-Modell, RunTemplate (v0.7.0)Run, RunPhase, RunOrchestrationService, RunTemplate, /runs
secretsAPI-Key-VerschlüsselungAES-GCM, /integrations
securitySpring SecuritySecurityConfig
settingsGlobale Defaults/einstellungen, Tabelle app_setting (V9); Setting execution.sandbox.variant seit v0.7.0
teamTeam-ZusammenstellungSerialisiert in AGENTS.md
validationBuild-Validierungmvn verify, npm run build
webUI-QuerschnittLayouts, Theme-Toggle, Dashboard
wizardProjekt-Assistent in sechs Schritten mit Kosten-SchätzungWizardController, WizardService, WizardCostEstimator, TemplateRegistry (18 Templates: 6 Backend, 11 Frontend, 1 Repo-Import), VersionLookupClient; Tabellen wizard_draft (V12), version_cache (V13)
sdlcBacklog mit Abhängigkeiten, Meilensteine, zeitgesteuerte RoutinenPlanItem, ProjectMilestone, Routine, /backlog
mandantMandantentrennung, Budget-Cap, KostenattributionTenantContext, /mandanten, /analytics/kosten
skillsVersionierte, mandanten-gescopte Skill-/Plugin-BibliothekSkillVersion, /skills
guardrailsVendor-neutrale Engineering-Guardrails als versionierte RessourceGuardrailsService, Projektion als AGENTS.md
provenanceWarum-Trace je Run (Blatt-Slice)/runs/{id}/trace
exportAudit-Export-Bundle (Blatt-Slice)/export
workflowWorkflow-Ebene über dem Run: Task-Graph, Child Runs, Workspace-Leases, Merge Queue, Integration Gate (Feature-Flag, Default aus)WorkflowService, LeaseService, MergeCoordinator, IntegrationService, /workflows
kennzahlenProduktivitäts- und Qualitätsmessung aus vorhandenen Daten (Blatt-Slice, kein eigenes Schema); nicht messbare Größen werden als benannte Lücke ausgewiesen statt mit einer Zahl gefülltKennzahlenService, Quote, Messluecke, /kennzahlen
contractContract Registry: versionierte Verträge mit Content-Hash, Strukturprüfung, Bruch-Erkennung (Blatt-Slice)ContractRegistry, ContractValidator, /vertraege

Datenfluss: vom Wizard bis zum Quality Gate

Wizardwizard_draft
Projectproject_def
Artefakteprompt_artifact
Runrun
run_phase
run_log
token_usage
Quality Gatequality_gate_run
  1. Wizard sammelt Antworten in wizard_draft (überlebt Browser-Reload und Server-Restart).
  2. ProjectDefinition hält alle Editor-Felder. Status DRAFTREADY nach Artefakt-Generierung.
  3. Artefakte werden vom PromptAssemblyService erzeugt, sind editierbar.
  4. Run orchestriert Phasen asynchron (@Async), UI hört per SSE.
  5. Quality Gate ruft Reviewer parallel auf, aggregiert zum Verdict.

Iterativer SDLC-Loop

Der Datenfluss oben beschreibt einen einzelnen Greenfield-Run bis zum Quality Gate. Über den einzelnen Run hinaus schließt die Plattform einen iterativen Loop über einen persistenten Workspace — dieselbe Projektkopie wird über mehrere Runs hinweg weitergebaut, frisch angelegt oder per Repo-Import eines bestehenden Repositories. Acht Schritte greifen ineinander:

  1. Planen. Ein Plan-Run erzeugt Markdown-Pläne unter plans/*.md im Workspace; daraus entsteht ein Backlog aus einzeln aktivierbaren Einträgen.
  2. Auswählen / Aktivieren. Ein Backlog-Eintrag wird im Backlog aktiviert und damit zum Ziel des nächsten Build-Runs.
  3. Bauen. Ein Build-Run läuft auf einem eigenen Branch (sdlc/run-…) im persistenten Workspace, statt direkt auf dem Base-Branch.
  4. Selbstkorrektur. Schlägt der Build fehl, wiederholt die run-Engine die Korrektur automatisch und begrenzt: der Run wechselt von NEEDS_CORRECTION zurück nach RUNNING, bis der Build grün ist oder das Wiederholungslimit erreicht ist.
  5. Quality-Gate in der Pipeline. Das qualitygate-Modul läuft als Pipeline-Schritt in drei Modi: aus, beratend (meldet nur) oder blockierend (stoppt die Auslieferung bei rotem Verdict).
  6. Liefern. Bei konfiguriertem Git-Remote und hinterlegtem GitHub-Token erzeugt das git-Modul einen Push des Branches und öffnet einen Pull-Request; ohne Remote/Token erfolgt ein lokaler Merge in den Base-Branch.
  7. Lernen. Erkenntnisse landen im Project-Memory und fließen als Kontext in Folge-Runs ein.
  8. Weiter. Optional erzeugt die Plattform Auto-Folgevorschläge für den nächsten Schritt — damit schließt sich der Loop zurück zu Schritt 1.

Persistenz: Postgres + Flyway

Schema-Änderungen ausschließlich über Flyway-Migrationen unter app/src/main/resources/db/migration/. Jede Migration nummeriert (V<n>__<name>.sql), unveränderlich nach Production.

MigrationenInhalt
V1–V9Grundschema: Projekte, Agenten, Teams, Runs, Phasen, Logs, Metriken, Budgets, Settings
V12–V18Wizard-Zustand, Versions-Cache, Modellwahl, Templates, Repo-Import, Projekt-Memory
V19–V25Plan-/Build-Runs, Branch-Isolation, Remote + Pull-Request, Korrekturversuche, Quality Gate, erlaubte Adapter
V26–V28Mandant, Nutzer-Mandanten-Bindung, Mandantenbudget
V29–V33Signierte Audit-Hashkette, Policy-as-Code, SBOM, SBOM-Signatur, Ketten-Checkpoint
V34–V37Auslöser-Herkunft, harte Mandanten-Isolation, Meilensteine, Backlog-Abhängigkeiten
V38–V39Skill-Bibliothek, Routinen
V40–V43Workflow-Ebene: Run-Verknüpfung, Workflow-Aggregat, Task-Graph, Artefakte und Budget
V44–V45Workspace-Leases mit Pfad-Besitzmodell, Merge Queue, Integration Gate
V46–V48Contract Registry mit Vertragsbindung, strukturierte Vertragsbefunde
V49–V50Planrevisionen beim Umplanen, Einordnung der Merge-Konflikte nach Art
V51Worker-Registrierung und Aufgabenanspruch der Koordinationsschicht
V52Testabdeckung je Build — Zeilen, Zweige, Quelle; bewusst NULL-fähig
V53Rollback-Erkennung: Merge-Commit als Anker, Rücknahme durch Revert
V54Freiwillig erfasster Aufwand je Freigabeentscheidung — nullable, weil leer „nicht erfasst“ heißt und nicht „null Minuten“
Stand v0.30.0: 58 Tabellen, 52 Migrationen. Die Nummern V10 und V11 sind historisch nicht vergeben — Lücken in der Nummernfolge sind bei Flyway unkritisch, das Umnummerieren einer bereits ausgelieferten Migration wäre es nicht.
Test-Strategie: Tests laufen gegen H2 mit Postgres-Kompatibilitätsmodus. Migrationen vermeiden deshalb Postgres-spezifische Features wie GENERATED ALWAYS AS.
Migration gegen Mapping (seit v0.30.0): H2 baut sein Schema aus demselben JPA-Mapping, gegen das geprüft wird — eine Abweichung zwischen Migration und Mapping kann dort per Konstruktion nicht auffallen. Zwei Tests schließen die Lücke: SchemaMappingTest vergleicht ohne Docker in Sekunden die Namen aller erwarteten Tabellen und Spalten, SchemaValidierungTest spielt via Testcontainers alle Migrationen in ein echtes Postgres ein und lässt Hibernate mit validate Typen und Längen prüfen. Beide sind gegen eine absichtlich eingebaute Abweichung verifiziert.

Sandbox-Varianten (ADR-0011)

Der Workspace ist projektpersistent — ein lokales Verzeichnis unter SOFTWAREFABRIK_WORKSPACES_ROOT/<project-slug>. Ein Verzeichnis je Run gibt es seit v0.13 nicht mehr: Ein Folge-Run soll auf dem Ergebnis des vorherigen aufsetzen können. Build-Runs zweigen dafür auf einen eigenen Branch sdlc/run-<id8> ab; Child Runs eines parallelen Workflows bekommen je einen eigenen git-Worktree. Der Adapter wird im jeweiligen Verzeichnis als eigener Prozess gestartet.

Local (Default)

LocalProcessSandbox setzt Umgebungsvariablen pro Prozess (kein System.setenv auf Plattform-Ebene), terminiert Prozesse hart bei Cancel/Timeout (destroyForcibly()), schreibt stdout und stderr zeilenweise in run_log plus parallel in den SSE-Stream.

Container pro Run (v0.7.0, ADR-0011 Variante B)

ContainerProcessSandbox startet jeden Agent in einem ephemeren Docker- oder Podman-Container mit:

  • --cpus 2 --memory 4g --pids-limit 512 als Resource-Cap
  • --read-only Root + /tmp als tmpfs
  • --network=none als Default
  • Bindmount auf den Workspace, -w /workspace

Aktiviert per Setting execution.sandbox.variant=container; fehlt Docker im PATH, fällt ExecutionSandboxFactory mit Log-Warnung auf die lokale Variante zurück.

Adapter-Registry

Adapter sind Spring-Beans, die ExecutionAdapter implementieren. ExecutionAdapterRegistry sammelt alle Beans in einer Map <Name → Adapter>.

Adapter-Resolution-Reihenfolge

  1. Run-spezifischer Override
  2. Projekt-Default (preferredAdapter)
  3. User-Default (USER-Setting)
  4. Globaler Default (GLOBAL-Setting)
  5. YAML-Default als letzter Fallback

Override-Reihenfolge PROJECT > USER > GLOBAL > YAML gilt überall in der Plattform, nicht nur für Adapter (SettingService).

Sicherheit & CVE-Hygiene

  • Bootstrap-Admin wird nur angelegt, wenn SOFTWAREFABRIK_ADMIN_PASSWORD explizit gesetzt ist. Keine Default-Passwörter.
  • BCrypt für Passwörter, CSRF an in allen state-ändernden Endpoints.
  • API-Keys werden mit AES-GCM verschlüsselt (Master-Key aus SOFTWAREFABRIK_SECRETS_MASTER_KEY) in der DB persistiert.
  • Trivy- und OWASP-Scans laufen pro CI-Build. .trivyignore mit dokumentierten Ausnahmen.
  • pgJDBC 42.7.11 (CVE-2026-42198 fix). docker-compose.yml pinnt Postgres auf 127.0.0.1:5432.

Observability

Seit v0.9.0 exportiert die Plattform Prometheus-Metriken (Observability Ebene 1). Die Micrometer-Registry liegt unter /actuator/prometheus und liefert JVM-, HTTP- und DB-Pool-Metriken. Hauptklasse: ObservabilityConfig.

  • Der Endpoint ist auf ROLE_ADMIN beschränkt und wird per HTTP-Basic gescraped — keine offene Metrik-Schnittstelle.
  • Ein gemeinsames application-Label trennt mehrere Instanzen im selben Prometheus.
  • Bewusst Ebene 1: keine Tracing-Pipeline, kein eigener Grafana-Stack im Lieferumfang.

Versions-Cache und @Scheduled-Jobs

Damit der Wizard immer aktuelle stabile Versionen vorbelegt, hält die Plattform einen Cache mit Versions-Lookups. Zwei @Scheduled-Jobs halten ihn frisch:

  • Täglich um 03:00 Uhr aktualisiert ein Job den Versions-Cache.
  • Täglich um 04:00 Uhr löscht ein zweiter Job abgelaufene wizard_draft-Einträge (TTL 30 Tage).

Die Lookups folgen dem Strategy-Pattern: ein VersionLookupClient-Interface, drei Implementierungen:

  • MavenCentralLookupClient — fragt search.maven.org/solrsearch/select für Java-Bibliotheken (Spring Boot, ArchUnit, OWASP Dependency-Check).
  • GithubReleasesLookupClient — holt das neueste Release-Tag aus der GitHub-API (z.B. Trivy CLI).
  • NpmRegistryLookupClient — fragt registry.npmjs.org für Node-Pakete (Playwright).

Cache-Keys sind fachliche Konstanten, keine technischen Pfade — z.B. spring-boot.3.x, archunit.latest, owasp.dependency-check.latest, trivy.cli.latest, playwright.npm.latest. So lässt sich die Quelle wechseln, ohne Konsumenten zu brechen. Staleness wird in Java berechnet (Schwelle 25 Stunden, Toleranzpuffer über den 03:00-Job): now - refreshed_at > 25h — bewusst keine Postgres-GENERATED-Spalte, damit das H2-Test-Profil funktional bleibt.

Admins inspizieren den Cache unter /einstellungen/wizard/versions: pro Eintrag Cache-Key, gelieferter Wert, refreshed_at, last_error und ein Stale-Badge; ein Knopf „Jetzt manuell refreshen“ triggert den Lookup synchron für genau diesen Key.

Settings-Architektur

Das Settings-Modul ist das Schaltbrett der Plattform. Drei Designentscheidungen greifen ineinander:

  1. Scope-Hierarchie: jedes Setting hat einen Scope (GLOBAL, USER, PROJECT). SettingService.resolve(key, context) sucht von eng nach weit: erst PROJECT, dann USER, dann GLOBAL, dann YAML.
  2. 5-Minuten-TTL-Cache: resolved Werte werden gecached, ein resolve kostet in 99 % der Fälle nichts. UI-Änderungen invalidieren nicht hart — nach maximal 5 Minuten ist der neue Wert da, ohne App-Restart.
  3. Audit-Trail: jede Schreiboperation auf app_setting erzeugt einen audit_event mit Subject, Key, Old-Value, New-Value.

Konkrete Settings, die heute verwaltbar sind:

KeyBedeutungTypischer Wert
workspace.rootWurzelverzeichnis für alle Run-Workspaces/var/softwarefabrik/workspaces
git.user.nameGit-Author für Auto-CommitsSoftware Factory Bot
git.user.emailGit-E-Mailbot@softwarefabrik.local
execution.adapter.defaultDefault-Execution-Adapterclaudecode
execution.model.claudecodeDefault-Modell für Claude Codeclaude-sonnet-5
execution.model.codexDefault-Modell für Codexgpt-5
execution.sandbox.variantSandbox-Variante (local / container)local
budget.tokens.dailyTages-Token-Cap2000000
budget.threshold.softSoft-Schwelle in Prozent (warnt nur)80

Live-Streaming und SSE

Die Run-Detail-Seite streamt drei Dinge live aus dem Backend in den Browser: Logs, Phasen-Updates und Token-Counter. Statt Polling nutzt die Plattform Server-Sent Events (SSE) — ein offener HTTP-Stream, der einseitig Push-Nachrichten an den Browser schickt.

  • Heartbeat: alle 20 Sekunden ein leerer :keepalive-Comment — hält die Verbindung lebendig und enttarnt tote TCP-Sockets.
  • Auto-Reconnect: die Browser-EventSource reconnectet selbst; die Plattform setzt retry: 5000 (5 s).
  • Lost-Tail-Replay: beim Reconnect schickt der Server zuerst die letzten 50 Log-Einträge nach, sodass keine Lücke entsteht.
  • Backpressure: jede SSE-Sitzung hat einen begrenzten Puffer; kommt der Browser nicht hinterher, wird die Verbindung sauber geschlossen statt den Heap volllaufen zu lassen.

Die Endpoints liegen unter /runs/{id}/stream/logs, /runs/{id}/stream/phases und /runs/{id}/stream/tokens. Blockt ein Reverse-Proxy HTTP-Streaming (manche Corporate-Setups tun das), schaltet die UI einen Polling-Fallback im 2-Sekunden-Takt an.

Test-Strategie und Coverage-Gate

Tests sind nicht optional, sondern Architekturbestandteil. Drei Säulen:

  • Unit-Tests: pro Service-Klasse, schnell, ohne Spring-Context, mit Mockito.
  • Integration-Tests: mit Spring-Boot-Test-Slices (@WebMvcTest, @DataJpaTest) gegen H2.
  • ArchUnit-Tests: pinnen Schicht- und Modulgrenzen. Wer aus web direkt in infrastructure greift, kriegt einen roten Test.

Coverage-Gate: JaCoCo ≥ 81 % Branch / ≥ 85 % Line. Wer einen Service ohne Test mergen will, kommt durch mvn verify nicht durch — absichtlich streng. CI-Pipeline (.github/workflows/ci.yml): Build + Test + ArchUnit + JaCoCo + OWASP Dependency-Check + Trivy-File-Scan. OWASP läuft non-blocking ohne NVD-Key, Trivy nur im dedizierten Job.

Was bewusst NICHT in der Architektur ist

Genauso wichtig wie das, was die Plattform tut, ist das, was sie absichtlich nicht tut. Diese Auslassungen halten V1 lesbar und ausbaubar:

  • Mandanten-Grundlagen, keine volle Mehrmandanten-Fähigkeit: Isolation an der Projektgrenze, RBAC und SSO sind vorhanden; die vollständige, auditsichere Mehrmandanten-Trennung (getrennte Policy/Audit/Reports pro Mandant) folgt — bis dahin: eine Instanz pro Mandant empfohlen. Siehe Known Limitations.
  • Keine eigene Web-IDE: die Plattform öffnet keinen Code-Editor im Browser. Du arbeitest auf dem Workspace mit deinen lokalen Tools (VS Code, IntelliJ, …).
  • Kein eigener Build-Server: mvn verify oder npm run build laufen im Workspace, nicht in einem Build-Cluster.
  • Kein eigener LLM-Hosting-Stack: die Plattform ruft nur Coding-CLIs (Claude Code, Codex, Kimi, Gemini, Aider). Air-Gap-Setups laufen über lokale CLI + lokales LLM (z.B. Ollama).
  • Keine Plugin-API für externe Adapter: Adapter werden in V1 als Java-Klasse im Code hinzugefügt, nicht als Plugin gesteckt. Eine Plugin-API ist im Backlog (ADR-0014).

Lizenz- und Identitätsschicht

Neben dem Produkt-Backend existiert ein eigener Stack für Identität und Lizenzierung, entkoppelt von der Plattform-DB:

  • Keycloak als OIDC-Provider (User, Rollen, Device-Authorization-Grant).
  • License-Service als Spring-Boot-Microservice mit eigener PostgreSQL.
  • Lease-JWT signiert mit EdDSA (Ed25519)nicht RS256 — mit eingebetteten Limits pro Tier.
  • Offline-Verifikation im Client gegen den öffentlichen Schlüssel unter https://license.softwarefabrik.io/api/v1/pubkey, fail closed bei abgelaufenem Lease.

Die drei Tiers sind Community, Professional und Enterprise. Community braucht keine Account-Registrierung (Lease über E-Mail + Device-Fingerprint-Hash). Die Lease-Dauer hängt vom Tier ab:

Tier / VarianteLease-Dauer
Community7 Tage
Professional30 Tage
Enterprise Cloud30 Tage
Enterprise Self-Hosted90 Tage
Enterprise Air-Gap365 Tage

Das Lease legt fest, welche Adapter verwendet werden dürfen und wie Run-Anlage und Team-Größe begrenzt sind. Details im Lizenzmodell.

Tiefere Hintergründe zur Architektur-Philosophie: Architektur-Whitepaper (76 Seiten Deep-Dive, Referenzarchitektur, Agenten-Orchestrierung, Guardrails). Versionsgeschichte mit ADRs pro Phase: News-Archiv.