Goal of the architecture
The platform is a control plane for AI-assisted software development. It bundles four tasks into one web app: project capture, artifact generation, run orchestration and quality assessment. Coding CLIs (Claude, Codex, Gemini, Aider) are invoked as subprocesses; their output is structured into the database and made live-visible in the UI.
Two principles, deliberately not buzzword-loaded in V1:
- Hexagonal per module. Every business module (e.g.
run,wizard,settings) has separate domain, application, web and infrastructure layers. Domain depends on nothing. - Local execution. One Spring Boot process + Postgres in Docker. No cluster, no Kafka, no service mesh.
Browser → Spring MVC + Thymeleaf → Application services → Domain model
↓
PostgreSQL ← Repositories ← Adapters
(Claude / Codex / Gemini / Aider / Mock / Git / FS)
Layers and modules
Inside the Maven module app all business modules live under io.softwarefabrik.app.<module>. Each module has at most four sub-packages:
domain/— entities, value objects, repository interfaces, business services. No Spring, JPA or web annotations.application/— use-case services. Transaction boundaries (@Transactional).web/— Spring MVC controllers, Thymeleaf bindings, DTOs, SSE endpoints.infrastructure/— JPA repositories, external adapters, filesystem access, scheduled jobs.
Dependency rules
domaindepends on nothing.applicationdepends only ondomain.webdepends onapplication, not directly ondomain.infrastructureimplementsdomainports.- No cross-grabbing into another module's
domain.
ArchUnit tests pin these rules. A wrong dependency turns CI red.
Modules in v0.19.0
Modules in detail
| Module | Purpose | Main classes / routes |
|---|---|---|
agent | Agent roles + preferredModel | AgentDefinition, AgentRoleService, /agents |
audit | Append-only audit log | AuditEvent, table audit_event |
common | Shared utilities | ID generator, clock abstraction |
conductor | Plugins/Skills sync, AGENTS.md writer (v0.6.0+) | PluginCatalog, SkillsCatalog, ConductorWorkspaceWriter |
execution | Run execution, sandbox factory, stream parser | ExecutionAdapterRegistry, LocalProcessSandbox + ContainerProcessSandbox, ExecutionSandboxFactory, ClaudeStreamJsonParser, TokenEstimator |
git | Git in the workspace, inline diff before approval | GitService.diffSeit() (v0.8.0), shell-out to git |
license | Tier licenses, JWT lease | /license, /admin/license |
observability | Prometheus metrics, observability level 1 (since v0.9.0) | ObservabilityConfig, Micrometer registry at /actuator/prometheus (ROLE_ADMIN) |
policy | Approval policies | ApprovalDecision |
projectdefinition | Project editor | /projects, /projects/{id}/edit |
prompt | Markdown artifact generation | Templates under resources/prompts/ |
qualitygate | Verdict aggregation | /runs/{id}/quality-gate |
review | Reviewer implementations | aider-review, claude-review, security, architecture-reviewer, hallucination-review |
run | Run model, RunTemplate (v0.7.0) | Run, RunPhase, RunOrchestrationService, RunTemplate, /runs |
secrets | API-key encryption | AES-GCM, /integrations |
security | Spring Security | SecurityConfig |
settings | Global defaults | /einstellungen, table app_setting (V9); setting execution.sandbox.variant since v0.7.0 |
team | Team composition | Serialised into AGENTS.md |
validation | Build validation | mvn verify, npm run build |
web | UI cross-cutting | Layouts, theme toggle, dashboard |
wizard | Four-step assistant with cost estimate | WizardController, WizardService, WizardCostEstimator, TemplateRegistry (6 templates); tables wizard_draft (V12), version_cache (V13) |
Data flow: from wizard to quality gate
run_phase
run_log
token_usage
- Wizard collects answers in
wizard_draft(survives browser reload and server restart). - ProjectDefinition holds all editor fields. Status
DRAFT→READYafter artifact generation. - Artifacts are generated by
PromptAssemblyService, editable. - Run orchestrates phases asynchronously (
@Async), UI listens via SSE. - Quality Gate invokes reviewers in parallel, aggregates to the verdict.
Iterative SDLC loop
The data flow above describes a single greenfield run up to the quality gate. Beyond the single run, the platform closes an iterative loop over a persistent workspace — the same project copy is built further across multiple runs, freshly created or imported from an existing repository via repo-import. Eight steps interlock:
- Plan. A plan run produces Markdown plans under
plans/*.mdin the workspace; from these a backlog of individually activatable items emerges. - Select / activate. A backlog item is activated in the backlog and becomes the objective of the next build run.
- Build. A build run runs on its own branch (
sdlc/run-…) in the persistent workspace, instead of directly on the base branch. - Self-correction. If the build fails, the
runengine repeats the correction automatically and in a bounded way: the run switches fromNEEDS_CORRECTIONback toRUNNINGuntil the build is green or the retry limit is reached. - Quality gate in the pipeline. The
qualitygatemodule runs as a pipeline step in three modes: off, advisory (reports only) or blocking (stops delivery on a red verdict). - Deliver. With a configured Git remote and a stored GitHub token the
gitmodule pushes the branch and opens a pull request; without remote/token it performs a local merge into the base branch. - Learn. Findings land in project memory and flow as context into follow-up runs.
- Continue. Optionally the platform produces auto follow-up proposals for the next step — closing the loop back to step 1.
Persistence: Postgres + Flyway
Schema changes exclusively through Flyway migrations under app/src/main/resources/db/migration/. Every migration numbered (V<n>__<name>.sql), immutable after production.
| Migration | Content |
|---|---|
V1 | Initial schema: project_definition, agent_definition, team, run, run_phase |
V2 | run_log, token_usage_event |
V3 | approval_policy, approval_decision |
V4 | integration_secret (encrypted) |
V5 | audit_event |
V6 | quality_gate_run, review_finding |
V7 | license, license_lease |
V8 | build_result |
V9 | v0.4.0: app_setting with scope column |
V10 | Run tags and quick-start markers |
V11 | v0.4.0: agent_preferred_model |
V12 | v0.4.0: wizard_draft |
V13 | v0.4.0: version_cache (Java-computed staleness, no DB computed column — H2 test profile stays functional) |
V14 | v0.6.0: extended agent_definition for conductor (mission, active skills) |
V15 | v0.7.0: run_template — adapter, team, objective, optional project scope, audit fields |
Sandbox variants (ADR-0011)
Every run gets its own workspace — a local directory under SOFTWAREFABRIK_WORKSPACES_ROOT/<project-slug>/<run-id>. The adapter is launched in that directory as its own process.
Local (default)
LocalProcessSandbox sets environment variables cleanly per process (no System.setenv at platform level), terminates processes hard on cancel/timeout (destroyForcibly()), writes stdout and stderr line-by-line into run_log plus SSE stream.
Container per run (v0.7.0, ADR-0011 variant B)
ContainerProcessSandbox starts every agent in an ephemeral Docker or Podman container with:
--cpus 2 --memory 4g --pids-limit 512as resource cap--read-onlyroot +/tmpas tmpfs--network=noneas default- Bindmount on the workspace,
-w /workspace
Enabled via setting execution.sandbox.variant=container; if Docker is missing from the PATH, ExecutionSandboxFactory falls back to the local variant with a log warning.
Security & CVE hygiene
- Bootstrap admin is only created when
SOFTWAREFABRIK_ADMIN_PASSWORDis explicitly set. No default passwords. - BCrypt for passwords, CSRF on every state-changing endpoint.
- API keys encrypted with AES-GCM (master key from
SOFTWAREFABRIK_SECRETS_MASTER_KEY) in DB. - Trivy and OWASP scans run per CI build.
.trivyignorewith documented exceptions. - pgJDBC 42.7.11 (CVE-2026-42198 fix).
docker-compose.ymlpins Postgres to127.0.0.1:5432.
Observability
Since v0.9.0 the platform exports Prometheus metrics (observability level 1). The Micrometer registry sits at /actuator/prometheus and serves JVM, HTTP and DB-pool metrics. Main class: ObservabilityConfig.
- The endpoint is restricted to ROLE_ADMIN and scraped via HTTP Basic — no open metrics surface.
- A shared
applicationlabel separates multiple instances in the same Prometheus. - Deliberately level 1: no tracing pipeline, no bundled Grafana stack.
Version cache and @Scheduled jobs
So the wizard can always pre-fill current stable versions, the platform keeps a cache of version lookups. Two @Scheduled jobs keep it fresh:
- Daily at 03:00 a job refreshes the version cache.
- Daily at 04:00 a second job deletes expired
wizard_draftrows (TTL 30 days).
Lookups follow the strategy pattern: a VersionLookupClient interface, three implementations:
MavenCentralLookupClient— queriessearch.maven.org/solrsearch/selectfor Java libraries (Spring Boot, ArchUnit, OWASP Dependency-Check).GithubReleasesLookupClient— fetches the latest release tag from the GitHub API (e.g. Trivy CLI).NpmRegistryLookupClient— queriesregistry.npmjs.orgfor Node packages (Playwright).
Cache keys are functional constants, not technical paths — e.g. spring-boot.3.x, archunit.latest, owasp.dependency-check.latest, trivy.cli.latest, playwright.npm.latest. That lets us swap the source without breaking consumers. Staleness is computed in Java (threshold 25 hours, a buffer over the 03:00 job): now - refreshed_at > 25h — deliberately not a Postgres GENERATED column, so the H2 test profile keeps working.
Admins inspect the cache at /einstellungen/wizard/versions: each row shows cache key, returned value, refreshed_at, last_error and a Stale badge; a "Refresh now" button triggers a synchronous lookup for that key.
Settings architecture
The settings module is the platform's control panel. Three design choices work together:
- Scope hierarchy: every setting belongs to a scope (
GLOBAL,USER,PROJECT).SettingService.resolve(key, context)searches narrow-to-wide: PROJECT first, then USER, then GLOBAL, then YAML. - 5-minute TTL cache: resolved values are cached, so a
resolveis essentially free 99 % of the time. UI changes don't hard-invalidate — at most 5 minutes later the new value is live, no app restart. - Audit trail: every write to
app_settingcreates anaudit_eventwith subject, key, old value, new value.
Concrete settings managed today:
| Key | Meaning | Typical value |
|---|---|---|
workspace.root | Root directory for all run workspaces | /var/softwarefabrik/workspaces |
git.user.name | Git author for auto-commits | Software Factory Bot |
git.user.email | Git email | bot@softwarefabrik.local |
execution.adapter.default | Default execution adapter | claudecode |
execution.model.claudecode | Default model for Claude Code | claude-sonnet-5 |
execution.model.codex | Default model for Codex | gpt-5 |
execution.sandbox.variant | Sandbox variant (local / container) | local |
budget.tokens.daily | Daily token cap | 2000000 |
budget.threshold.soft | Soft threshold in percent (warns only) | 80 |
Live streaming and SSE
The run detail page streams three things live from the backend to the browser: logs, phase updates and token counter. Instead of polling the platform uses Server-Sent Events (SSE) — an open HTTP stream that pushes one-way messages to the browser.
- Heartbeat: every 20 seconds an empty
:keepalivecomment — keeps the connection alive and exposes dead TCP sockets quickly. - Auto-reconnect: the browser EventSource reconnects on its own; the platform sets
retry: 5000(5 s). - Lost-tail replay: on reconnect the server first replays the last 50 log entries so no gap appears.
- Backpressure: each SSE session has a bounded buffer; if the browser falls behind the connection is closed cleanly instead of letting the heap grow.
Endpoints sit at /runs/{id}/stream/logs, /runs/{id}/stream/phases and /runs/{id}/stream/tokens. If a reverse proxy blocks HTTP streaming (some corporate setups do), the UI falls back to a 2-second polling loop with the same payload.
Test strategy and coverage gate
Tests are not optional; they are part of the architecture. Three pillars:
- Unit tests: per service class, fast, no Spring context, Mockito-based.
- Integration tests: with Spring Boot test slices (
@WebMvcTest,@DataJpaTest) against H2. - ArchUnit tests: pin layer and module boundaries. Reach from
webstraight intoinfrastructureand a test fails.
Coverage gate: JaCoCo ≥ 81 % branch / ≥ 85 % line. A service merged without tests doesn't pass mvn verify — intentionally strict. CI pipeline (.github/workflows/ci.yml): build + test + ArchUnit + JaCoCo + OWASP Dependency-Check + Trivy file scan. OWASP runs non-blocking without an NVD key, Trivy only in its dedicated job.
What is deliberately NOT in the architecture
What the platform omits is as important as what it does. These omissions keep V1 readable and extensible:
- Tenant foundations, not full multi-tenancy: project-boundary isolation, RBAC and SSO are in place; full audit-grade multi-tenancy (separate policy/audit/reports per tenant) is still to come — until then, one instance per tenant is recommended. See Known Limitations.
- No own web IDE: the platform doesn't open a code editor in the browser. You work on the workspace with your local tools (VS Code, IntelliJ, …).
- No own build server:
mvn verifyornpm run buildrun inside the workspace, not on a build cluster. - No own LLM hosting: the platform only invokes coding CLIs (Claude Code, Codex, Gemini, Aider). Air-gap setups go local CLI + local LLM (e.g. Ollama).
- No plugin API for external adapters: in V1 adapters are added as Java classes in the codebase, not loaded as plugins. A plugin API is on the backlog (ADR-0014).
License and identity stack
Beside the product backend there is a separate stack for identity and licensing, decoupled from the platform DB:
- Keycloak as the OIDC provider (users, roles, device authorization grant).
- License service as a Spring Boot microservice with its own PostgreSQL.
- Lease JWT signed with EdDSA (Ed25519) — not RS256 — with embedded per-tier limits.
- Offline verification in the client against the public key at
https://license.softwarefabrik.io/api/v1/pubkey, fail closed on lease expiry.
The three tiers are Community, Professional and Enterprise. Community needs no account registration (lease issued via email + device-fingerprint hash). Lease duration depends on the tier:
| Tier / variant | Lease duration |
|---|---|
| Community | 7 days |
| Professional | 30 days |
| Enterprise Cloud | 30 days |
| Enterprise Self-Hosted | 90 days |
| Enterprise Air-Gap | 365 days |
The lease determines which adapters may be used and how run creation and team size are limited. Details in the license model.