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
| Assembly | Main class | Runs | Notes |
|---|---|---|---|
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
| Area | Modules |
|---|---|
| Platform / tenancy | tenancy (provisioning, plan limits, upgrades, erasure), security (auth, tenant context, plan enforcement), admin, platform, billing |
| Agent runtime | agents, chat, providers, eval |
| Automation | workflow, eventbus (Kafka-backed outbox), approval |
| Knowledge | knowledge (hybrid vector + keyword, RRF fusion), graphmemory (Apache AGE) |
| Governance | guardrails, audit, mcp (dynamic connector registry) |
| Channels / devices | omnichannel, devices, sarvam (India voice/IVR), imagegen, elevenlabs, google |
| Infrastructure | storage (S3 API or local disk), crypto (APP_ENCRYPTION_KEY), sdlc, codeintel (bridge — off by default) |
| Assembly | app — 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
| Service | Stack | Port | Role | How to enable |
|---|---|---|---|---|
web | Next.js | 3000 | Tenant console + public site | Always (app.<your-domain> with a domain) |
web-admin | Next.js | 3001 | Platform-operator console | Full tier / Helm (admin.<your-domain>) |
sidecar | Python 3.12 + FastAPI | 8001 | Chunking, embeddings, OCR/extraction, reranking, transcription | On in Full; Compose profile rag in Lite; internal only |
code-intel | Python FastAPI | 8090 | Codebase RAG / SDLC tooling | Profile code-intel; core's bridge also needs CODE_INTEL_ENABLED=true |
code-intel-docker | Docker-in-Docker | — | Privileged sandboxed analysis executor for code-intel | With profile code-intel (privileged) |
app-builder | Python FastAPI | 8091 | Generates small apps from a prompt | Profile builder (implies code-intel) |
mcp-stdio-runner | Node | 8097 | Spawns untrusted MCP STDIO packages outside core | Profiles tools / browser / sandbox |
tool-sandbox | Python + Java | 8095 | Executes tenant-authored code with Linux isolation | Profile sandbox + TOOL_SANDBOX_ENABLED=true |
voice-agent | Python LiveKit worker | — | Voice channel runtime | Profile voice (with livekit) |
rl-trainer | Python FastAPI | 8095 | RL policy training for agent tuning | Profile 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.
- Core's code-intel bridge (
code-intel.enabled=falseby default; needsCODE_INTEL_*env to enable). - The Kafka event bus (Redpanda) — only with Compose profile
eventsortemporal; theworkflowSpring profile needs it. app-workerandapp-jobsas separate processes — the assemblies exist; Lite and the default Full tier run everything in the onecorecontainer.
Sources
README.md §1, core/pom.xml, deploy/compose/docker-compose.yml, deploy/helm/charts/, deploy/README.md.