ADR 0003 — Task Management Application Architecture¶
Date: 2026-04-01 Status: Proposed Deciders: Chris Relates to: ADR 0002 — Task Management Agent Integration
Context¶
ADR 0002 selected Option B: a Kubernetes-hosted, isolated task management application with a REST API for agent integration and a management UI. This ADR defines the internal architecture of that application — how the UI, API, persistence layer, authentication, and agent integration surface are structured and deployed within the cluster.
The application must:
- Expose a REST API consumable by AI agents
- Provide a management UI for humans to create, view, and manage tasks
- Persist task state in PostgreSQL (provisioned via CloudNativePG)
- Authenticate users via the existing Keycloak instance — all members of the Skatzi Keycloak group get access; no fine-grained RBAC in v1
- Run entirely within the Skatzi Kubernetes cluster with no external dependencies
- Be deployable using the existing Flux CD + Kustomize GitOps pipeline
Is this a microservice?¶
Yes. Each component (UI, API, and optionally an MCP server in v2) is an independently deployable service with a single responsibility, its own container image, and its own scaling properties. The services communicate over well-defined HTTP interfaces. This follows the microservice architectural pattern, hosted within a single Kubernetes namespace rather than spanning multiple clusters.
Authentication¶
All options share the same authentication model: Keycloak OIDC. The UI initiates an OIDC login flow against the cluster's Keycloak instance. On completion, the browser holds a JWT access token. Every API request from the UI (or from an AI agent) must include this token as a Bearer header. The API validates the token against Keycloak's public keys and checks group membership. In v1, any valid user in the Skatzi Keycloak group is granted full access — there is no role-based access control.
Options Considered¶
Option A — Monolithic Single-Service¶
A single container serves both the management UI and the REST API from the same process. The UI is either server-side rendered or bundled as static files served by the same HTTP server. Authentication is handled by the monolith, which validates JWTs from Keycloak.
graph TD
Agent["🤖 AI Agent"]
Browser["🌐 Browser"]
KC["Keycloak\n(OIDC)"]
subgraph Kubernetes Cluster
subgraph tasks namespace
Mono["platform-tasks\n(UI + API)"]
DB[("PostgreSQL\nCloudNativePG")]
end
end
Browser -->|OIDC login| KC
KC -->|JWT| Browser
Agent -->|REST API + Bearer JWT| Mono
Browser -->|HTTP + Bearer JWT| Mono
Mono -->|validate token| KC
Mono -->|SQL| DB
Pros: - Simplest deployment — one Deployment, one Service, one Ingress, one image - No inter-service communication to configure - Fast to bootstrap for a prototype
Cons: - UI and API are tightly coupled — hard to evolve or replace independently - Cannot scale the UI and API tiers separately - Technology choice for the UI constrains the API runtime or adds build complexity - A single broken change can take down both the human-facing UI and the agent-facing API
Option B — Decoupled Frontend + Backend API (Microservices)¶
The frontend (a React SPA served via nginx) and the backend API are separate microservices with separate Deployments. The frontend handles the Keycloak OIDC login flow and passes the JWT to the API on every request. The API validates the token and owns all business logic and database access.
graph TD
Agent["🤖 AI Agent"]
Browser["🌐 Browser"]
KC["Keycloak\n(OIDC)"]
subgraph Kubernetes Cluster
subgraph tasks namespace
UI["platform-tasks-ui\n(React SPA · nginx)"]
API["platform-tasks-api\n(REST API)"]
DB[("PostgreSQL\nCloudNativePG")]
end
Ingress["Ingress\n/* → UI\n/api/* → API"]
end
Browser -->|OIDC login| KC
KC -->|JWT| Browser
Browser -->|HTTPS| Ingress
Agent -->|REST /api/* + Bearer JWT| Ingress
Ingress --> UI
Ingress --> API
UI -->|REST /api/* + Bearer JWT| Ingress
API -->|validate token| KC
API -->|SQL| DB
Pros: - Clear separation of concerns — each service is independently deployable and scalable - The REST API is the single authoritative integration surface for both the UI and agents - The frontend can be replaced without touching the API - Aligns with the microservice pattern — two small, focused services
Cons: - Two Deployments and two images to maintain - Requires CORS configuration or shared Ingress routing - Slightly higher initial setup overhead than Option A
Option C — Backend API + MCP Server (Extended Microservices)¶
Extends Option B by adding a dedicated MCP (Model Context Protocol) server as a third microservice. AI agents connect via the MCP protocol rather than raw REST. The MCP server validates agent tokens against Keycloak before forwarding requests to the API. The management UI continues to use the REST API directly.
graph TD
Agent["🤖 AI Agent"]
Browser["🌐 Browser"]
KC["Keycloak\n(OIDC)"]
subgraph Kubernetes Cluster
subgraph tasks namespace
UI["platform-tasks-ui\n(React SPA · nginx)"]
API["platform-tasks-api\n(REST API)"]
MCP["platform-tasks-mcp\n(MCP Server)"]
DB[("PostgreSQL\nCloudNativePG")]
end
Ingress["Ingress\n/* → UI\n/api/* → API\n/mcp → MCP"]
end
Browser -->|OIDC login| KC
KC -->|JWT| Browser
Browser -->|HTTPS| Ingress
Agent -->|MCP + Bearer JWT| Ingress
Ingress --> UI
Ingress --> API
Ingress --> MCP
UI -->|REST /api/* + Bearer JWT| Ingress
MCP -->|validate token| KC
MCP -->|REST internal| API
API -->|SQL| DB
Pros: - Purpose-built agent integration surface — MCP is the emerging standard for LLM tool use - REST API remains clean and human-oriented; the MCP server handles agent-specific schemas, tool definitions, and streaming - Each microservice has a single responsibility and can evolve independently - Enables richer agent capabilities out of the box (structured tool calls, cancellation, progress)
Cons: - Three microservices and three images to build and maintain - MCP server adds operational complexity (health checks, versioning, auth) - MCP ecosystem tooling is still maturing as of early 2026 - Highest initial effort; overkill for a v1 with no external agent integrations yet
Option D — AgentHub Control Plane (Agent Registry + Config Server)¶
The long-term goal is to automate entire business workflows using a fleet of specialised agents. As the number of agent types grows, managing their identities, system prompts, tool definitions, and LLM configurations locally becomes unscalable. Option D introduces a dedicated AgentHub microservice — an agent control plane that sits alongside Taskbase and owns everything about the agents themselves.
Each agent (MCP server or autonomous worker, running anywhere — locally, in Kubernetes, or on another machine) bootstraps by calling AgentHub on startup to receive its full configuration. AgentHub also tracks which agents are currently connected, providing a live overview of the agent fleet.
Separation of concerns:
| Service | Owns |
|---|---|
| Taskbase | Work to be done — tasks, queue, history, token budgets |
| AgentHub | Who does the work — agent identities, system prompts, tool definitions, LLM config, connection state |
Agent bootstrap flow:
- Local MCP server starts with only two pieces of local config: the AgentHub URL and an API key
- Calls
GET /agents/{id}/config→ receives system prompt, tool definitions, taskbase URL, LLM model settings - Registers itself as connected via
POST /agents/{id}/connections - Begins polling Taskbase for tasks using the fetched configuration
- Sends periodic heartbeats; AgentHub marks the instance offline if heartbeats stop
LLM portability is a first-class concern in this model. AgentHub stores the LLM provider configuration per agent type — base_url, model, and the name of the secret to resolve. Swapping from Claude to a local Ollama instance requires a single config update in AgentHub with no changes to agent code.
graph TD
Browser["🌐 Browser"]
KC["Keycloak\n(OIDC)"]
Agent1["🤖 Deployment Agent\n(local MCP server)"]
Agent2["🤖 Research Agent\n(local MCP server)"]
Agent3["🤖 Finance Agent\n(local MCP server)"]
subgraph Kubernetes Cluster
subgraph tasks-dev namespace
TasksUI["platform-tasks-ui\n(React SPA)"]
TasksAPI["platform-tasks-api\n(REST API)"]
TasksDB[("PostgreSQL\ntaskbase")]
end
subgraph agenthub namespace
HubUI["agenthub-ui\n(React SPA)"]
HubAPI["agenthub-api\n(REST API)"]
HubDB[("PostgreSQL\nagenthub")]
end
GW["Gateway\ntaskbase.dev.skatzi.com\nagenthub.dev.skatzi.com"]
end
Browser -->|OIDC login| KC
Browser -->|HTTPS| GW
GW --> TasksUI
GW --> TasksAPI
GW --> HubUI
GW --> HubAPI
TasksAPI -->|SQL| TasksDB
HubAPI -->|SQL| HubDB
Agent1 -->|bootstrap config| HubAPI
Agent2 -->|bootstrap config| HubAPI
Agent3 -->|bootstrap config| HubAPI
Agent1 -->|GET /tasks/next| TasksAPI
Agent2 -->|GET /tasks/next| TasksAPI
Agent3 -->|GET /tasks/next| TasksAPI
AgentHub API surface:
| Endpoint | Purpose |
|---|---|
GET /agents |
List all registered agent types |
POST /agents |
Register a new agent type |
GET /agents/{id}/config |
Fetch full config — system prompt, tools, LLM settings |
PUT /agents/{id}/config |
Update agent config (triggers reload on next heartbeat) |
POST /agents/{id}/connections |
Agent instance registers as connected |
DELETE /agents/{id}/connections/{instanceId} |
Agent disconnects |
GET /agents/{id}/connections |
See all live instances of an agent type |
Pros:
- Central visibility across the entire agent fleet — which agents exist, which are online, what they are configured to do
- Update system prompts and tool definitions from the UI without touching local files or redeploying
- Multiple instances of the same agent type share one source of truth
- LLM provider is a per-agent config value — switching from Claude to a local LLM is a single update
- Taskbase and AgentHub remain decoupled — either can evolve independently
- Scales naturally as new agent types are added to automate additional business functions
Cons:
- Adds a fourth microservice and a second database (two Deployments, two images, two CNPG clusters)
- Introduces a startup dependency — agents cannot initialise if AgentHub is unreachable (mitigated by local config cache)
- Bootstrap problem is not eliminated, only reduced — each agent still needs AgentHub URL and credentials locally
- Significant build effort relative to v1 scope
Decision¶
Option B — Decoupled Frontend + Backend API is selected for v1.
The two-microservice split (UI + API) provides the right foundation without overbuilding. Keycloak OIDC is integrated from the start — the UI handles the login flow and all API requests (whether from the UI or from agents) carry a Bearer JWT validated by the API. All Skatzi Keycloak group members get full access with no RBAC in v1.
The target v2+ architecture is Option D (AgentHub). The long-term goal is to automate entire business workflows with a fleet of specialised agents, and local config management does not scale to that. Option C (MCP server) may be introduced alongside or as part of AgentHub rather than as a standalone service — the MCP surface can be served by AgentHub's API directly, providing tool definitions to agents that connect via the MCP protocol.
The v1 REST API should be designed with this future in mind: stable schemas, consistent error shapes, and no Claude-specific fields in the task or agent data model.
Consequences¶
- Two microservices in
components/tasks/:platform-tasks-apiandplatform-tasks-ui - Two Harbor images:
harbor.prod.skatzi.com/skatzi/platform-tasks-apiandharbor.prod.skatzi.com/skatzi/platform-tasks-ui - A CloudNativePG
Clusterresource within thetasksnamespace for persistence - A single Ingress routing
/api/*toplatform-tasks-apiand/*toplatform-tasks-ui - Keycloak OIDC integration from day one:
- The UI registers as a Keycloak client and handles the browser login flow
- The API validates Bearer JWTs on every request and checks Skatzi group membership
- No RBAC in v1 — group membership is the only gate
- A Flux
Kustomizationundercluster/hetzner-mgmt/tasks/to reconcile the component - REST API design must remain LLM-agnostic — no Claude-specific fields; token usage is reported as generic input/output counts
- REST API design should keep agent ergonomics in mind from the start (stable schemas, pagination, consistent error shapes) to ease a future MCP or AgentHub integration layer
- Option C (MCP server) and Option D (AgentHub) are deferred to v2 and do not affect the v1 component structure
- Application code will live in the
Skatzi/taskbaserepository - A future
Skatzi/agenthubrepository will house the AgentHub service when v2 begins