Skip to content

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:

  1. Local MCP server starts with only two pieces of local config: the AgentHub URL and an API key
  2. Calls GET /agents/{id}/config → receives system prompt, tool definitions, taskbase URL, LLM model settings
  3. Registers itself as connected via POST /agents/{id}/connections
  4. Begins polling Taskbase for tasks using the fetched configuration
  5. 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-api and platform-tasks-ui
  • Two Harbor images: harbor.prod.skatzi.com/skatzi/platform-tasks-api and harbor.prod.skatzi.com/skatzi/platform-tasks-ui
  • A CloudNativePG Cluster resource within the tasks namespace for persistence
  • A single Ingress routing /api/* to platform-tasks-api and /* to platform-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 Kustomization under cluster/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/taskbase repository
  • A future Skatzi/agenthub repository will house the AgentHub service when v2 begins