Docs/Architecture/System Architecture

System Architecture

VegaDūta is a modular monolith — one Spring Boot 3.4 / Java 21 deployable assembled from ~42 Maven modules — surrounded by a small set of satellite services for the work a JVM should not do (Python ML, untrusted-code execution, voice) and the work frontends do (two Next.js consoles). This page is the layered view; Service Topology is the deployment view.

The layered picture

flowchart TB
    subgraph edge["Edge"]
        CF["Your DNS / optional CDN"]
        ACAI["Caddy (Compose domain overlay) or Kubernetes Ingress — TLS"]
    end

    subgraph front["Frontends"]
        WEB["web — tenant console :3000"]
        WADM["web-admin — platform console :3001"]
        MOB["external API / OAuth clients"]
    end

    subgraph core["core — the modular monolith (one JAR)"]
        direction TB
        CAPI["REST surface — ~193 controllers, SSE/AG-UI, webhooks, inbound MCP, A2A"]
        subgraph mods["Modules by concern"]
            MAG["agents · chat · providers · eval"]
            MWF["workflow · eventbus · approval"]
            MKN["knowledge (vector+keyword, RRF) · graphmemory (AGE)"]
            MGOV["guardrails · audit · mcp registry"]
            MCH["omnichannel · devices · sarvam · imagegen · elevenlabs · google"]
            MPL["tenancy · security · admin · platform · billing"]
            MINF["storage · crypto · sdlc · codeintel bridge"]
        end
        PGB["pgbouncer (session mode, :6432; Full tier)"]
    end

    subgraph sat["Satellite services"]
        SC["sidecar — knowledge/ML :8001"]
        CI["code-intel — codebase RAG :8090"]
        AB["app-builder :8091"]
        MCPR["mcp-stdio-runner :8097"]
        VA["voice-agent (LiveKit)"]
        TS["tool-sandbox :8095"]
        RL["rl-trainer :8095 (opt-in)"]
    end

    subgraph data["Data & platform services"]
        PG[("Postgres 16 + pgvector + AGE — schema-per-tenant")]
        RD[("Valkey (Redis API)")]
        RP[("Redpanda (Kafka API)")]
        TMP["Temporal"]
        KC["Keycloak 26 (realm agentic-ai)"]
        MIN["SeaweedFS / any S3 API"]
        LF["Langfuse (LLM tracing)"]
    end

    subgraph ext["External"]
        BYOK["Tenant BYOK LLM providers (~14)"]
        POOL["Operator-configured platform pool (optional)"]
    end

    CF --> ACAI --> WEB
    CF --> ACAI --> WADM
    CF --> ACAI --> CAPI
    MOB --> CAPI
    WEB --> CAPI
    WADM --> CAPI
    CAPI --> mods
    mods --> PGB --> PG
    mods --> RD
    mods --> RP
    mods --> TMP
    mods --> KC
    mods --> MIN
    mods --> SC
    mods --> MCPR
    mods --> TS
    mods --> LF
    MKN --> SC
    MINF -. bridge off by default .-> CI
    MCH --> VA
    MAG --> BYOK
    MAG --> POOL
  

Why a modular monolith

The choice is deliberate and documented (ADR-0001): the team's constraint was deployment operability and data-consistency under multi-tenancy, not service-count. One JAR gives a single Flyway migrator, a single security context, in-process calls between agent / workflow / tenancy logic, and one artifact to build, scan and roll back. The module boundaries are drawn so a later extraction is possible — the deployment-shapes work (below) already splits background work onto separate JVMs without splitting the codebase.

What is not in the monolith is equally deliberate: anything that executes untrusted code (MCP STDIO packages, tenant-authored code), the Python ML stack, and the per-channel bridges live in satellites so core's process boundary is also its trust boundary.

The three deployable assemblies

AssemblyMain classRunsNotes
core/app CoreApplication The HTTP surface — all ~193 REST controllers, SSE/AG-UI streams, webhooks — plus, in a single-process deployment, every scheduled job too The core image; the only assembly in the Lite tier and the default Full tier (port 8080)
core/app-worker WorkerApplication Temporal workflow worker — executes durable workflow activities Defined and built (core/pom.xml); gated by app.workflow.temporal.worker-enabled; no @EnableScheduling. Runs as core-worker with Compose profile temporal or Helm global.temporal.enabled.
core/app-jobs JobsApplication The ~30 scheduled sweeps — job dispatch, outbox relays, channel pollers, quota resets Its own application-jobs.yml; runs as core-jobs with Compose profile jobs or the Helm chart deploy/helm/charts/core-jobs (global.components.coreJobs, default on).

The same split also exists inside one image as Spring profiles: shape-api (serve traffic, skip the sweeps) and shape-jobs (run the sweeps, skip request-serving concerns), defined in core/app-config. With neither profile set, one JVM runs everything — which is what the Lite tier and the default Full tier do: the single core container serves requests and runs the sweeps.

Module map of the monolith

AreaModules
Platform / tenancytenancy (provisioning, plan limits, upgrades, erasure), security (auth, tenant context, plan enforcement), admin, platform, billing
Agent runtimeagents, chat, providers, eval
Automationworkflow, eventbus (Kafka-backed outbox), approval
Knowledgeknowledge (hybrid vector + keyword, RRF fusion), graphmemory (Apache AGE)
Governanceguardrails, audit, mcp (dynamic connector registry)
Channels / devicesomnichannel, devices, sarvam (India voice/IVR), imagegen, elevenlabs, google
Infrastructurestorage (S3 API or local disk), crypto (APP_ENCRYPTION_KEY), sdlc, codeintel (bridge — off by default)
Assemblyapp — the Spring Boot assembly that runs; app-worker, app-jobs — alternate assemblies from the same modules

The full per-module catalogue with dependency edges is on Core Module Catalogue.

Satellite services

ServiceStackPortRoleHow to enable
webNext.js3000Tenant console + public siteAlways (app.<your-domain> with a domain)
web-adminNext.js3001Platform-operator consoleFull tier / Helm (admin.<your-domain>)
sidecarPython 3.12 + FastAPI8001Chunking, embeddings, OCR/extraction, reranking, transcriptionOn in Full; Compose profile rag in Lite; internal only
code-intelPython FastAPI8090Codebase RAG / SDLC toolingProfile code-intel; core's bridge also needs CODE_INTEL_ENABLED=true
code-intel-dockerDocker-in-Docker—Privileged sandboxed analysis executor for code-intelWith profile code-intel (privileged)
app-builderPython FastAPI8091Generates small apps from a promptProfile builder (implies code-intel)
mcp-stdio-runnerNode8097Spawns untrusted MCP STDIO packages outside coreProfiles tools / browser / sandbox
tool-sandboxPython + Java8095Executes tenant-authored code with Linux isolationProfile sandbox + TOOL_SANDBOX_ENABLED=true
voice-agentPython LiveKit worker—Voice channel runtimeProfile voice (with livekit)
rl-trainerPython FastAPI8095RL policy training for agent tuningProfile rl (opt-in)

How the two canonical flows traverse the layers

A console page load

Browser
  → Caddy (Compose domain overlay) or Kubernetes Ingress, TLS for *.<your-domain>
    → web (Next.js, server-rendered)

Browser (client-side JS)
  → api.<your-domain> (or localhost:8080) → core:8080
    → pgbouncer:6432 (Full tier, session pooling)
      → postgres:5432 (bundled) or your managed PostgreSQL

Core reaches the database through compose DNS (DB_HOST/DB_PORT); internal service names such as pgbouncer and keycloak:8080 are correct and must not be "fixed" to public URLs.

An agent turn

core → guardrails
     → tool resolution (MCP + custom code + built-ins)
     → LLM provider (tenant BYO key, or the platform hosted pool)
     → LlmMetricsAdvisor → llm_call_log row (in the tenant's schema)
                         → OTLP span → Langfuse (profile tracing, optional)

Identity at a glance

Keycloak 26, realm agentic-ai. Clients — verified from the checked-in realm JSON: agentic-ai-web (public, browser PKCE), agentic-ai-core (confidential; the only client that can mint a token from username + password via direct-access grants), agentic-ai-platform-admin, agentic-ai-mobile, agentic-ai-app, agentic-ai-ide (the last three are reserved for external clients that are not part of this repository). Realm roles: tenant-admin, platform-admin, agent-builder, delegated-user, end-customer. Because Keycloak pins its hostname, core validates tokens under a dual issuer: it validates the issuer Keycloak stamps (PUBLIC_AUTH_URL/realms/agentic-ai), and KEYCLOAK_ADDITIONAL_ISSUERS adds others, for example the old issuer during a domain change. See Identity & Access.

Off by default
  • Core's code-intel bridge (code-intel.enabled=false by default; needs CODE_INTEL_* env to enable).
  • The Kafka event bus (Redpanda) — only with Compose profile events or temporal; the workflow Spring profile needs it.
  • app-worker and app-jobs as separate processes — the assemblies exist; Lite and the default Full tier run everything in the one core container.

Sources

README.md §1, core/pom.xml, deploy/compose/docker-compose.yml, deploy/helm/charts/, deploy/README.md.