Solution Architecture Document โ Deployment Dashboard¶
Version: 1.0
Status: Draft
Date: May 2026
1. Problem Statement¶
Teams using any CI/CD tool (GitHub Actions, Azure DevOps, Jenkins, GitLab CI, etc.) lack a unified, at-a-glance view of what version of each service is currently deployed to each environment. Built-in deployment views in individual CI/CD tools typically show one row per environment with no per-service granularity, or require navigating across multiple pipelines and logs to reconstruct the full picture. As the number of services and environments grows, determining the state of the entire system becomes increasingly manual and error-prone.
Core question the system must answer:
"What version of service X is running in environment Y right now โ and did the last deployment succeed?"
2. Goals¶
- Show a real-time:
- services ร environments deployment matrix
- graph of deployments of different environments per service
sourced from CI/CD pipeline events (GitHub Actions, Azure DevOps, Jenkins, GitLab CI, or any tool that can make an HTTP POST)
- Show a real-time *graph of deployments of different environments per service
- Record a per-slot history of deployments (version, status, actor, time, run link)
- Require no changes to existing CI/CD pipelines beyond adding a single notification step
- Support SSE fan-out across multiple backend instances โ all connected browser clients receive live updates regardless of which instance handled the ingest request; no sticky sessions required
- Support any CI/CD tool, repository, and set of services/environments โ no hardcoded values; services and environments are discovered from stored data
3. Non-Goals¶
- Triggering or managing deployments (the system is read-only / notification-only)
- Acting as a CI/CD engine โ the backend only tracks deployment state pushed to it; it does not query any CI/CD tool. An optional, separately-deployed
Dashboard.Fetchercomponent (per CR-0009; see ยง7 "Dashboard.Fetcher (optional pull-mode adapter)") MAY translate pull โ push by polling a CI/CD tool's API and posting events toPOST /api/deploymentslike any other pusher; the backend's tool-agnostic contract is preserved because the fetcher reuses the same push endpoint and the sameX-Api-Key. The backend is never extended with CI/CD-specific SDKs. - Multi-organisation or multi-repository aggregation (out of scope for MVP)
- Role-based access control (the dashboard is internal read-only tooling)
4. Functional Requirements¶
| ID | Requirement |
|---|---|
| FR-01 | The system shall display a real-time deployment matrix organised by service (one row per service), showing the current state of each (service, environment) slot. |
| FR-02 | Each slot shall be capable of showing: version, status (success / in-progress / failure), actor, elapsed time since deployment, and a link to the CI/CD run. |
| FR-03 | When the current state is in-progress or failed, the slot shall also show the last successfully deployed version in a split section below the current state. |
| FR-04 | The system shall maintain a full deployment history per slot and expose it on demand via a history drawer. |
| FR-05 | The system shall receive deployment events through a push-based HTTP ingest API |
| FR-06 | Integrating the notify step shall require no changes to existing CI/CD pipelines beyond adding a single step. |
| FR-07 | The dashboard shall support filtering by service name and by failure state only. |
| FR-08 | All connected browser clients shall receive live updates when a new deployment event is ingested โ no page reload required. |
| FR-09 | The system shall support any set of services and environments without hardcoded values; the service and environment lists shall be derived from stored data. |
| FR-10 | The ingest API shall authenticate every write request with an API key; requests with a missing or invalid key shall be rejected with HTTP 401. |
| FR-11 | (v2.0) A desktop notification client shall alert developers via OS notifications when a deployment slot changes state, with a click-through to the dashboard. |
5. Non-Functional Requirements¶
| ID | Requirement |
|---|---|
| NFR-01 | All infrastructure shall run on Microsoft Azure. |
| NFR-02 | Total Azure infrastructure cost shall not exceed $30/month. |
| NFR-03 | Live updates shall be delivered to all connected clients within 5 seconds of a successful ingest event. |
| NFR-04 | The system is internal tooling โ no public internet exposure is required. The SPA itself is read-only against the API and does not handle authentication secrets. The ingest endpoint (POST /api/deployments) is reserved for CI/CD tooling. The dev-environment fake API key is never embedded in the SPA bundle. |
| NFR-05 | The backend shall be stateless; any number of instances may run behind a load balancer without sticky sessions. |
| NFR-06 | All infrastructure shall be defined as code using Terraform. |
| NFR-07 | Deployment history shall be retained for a minimum of 90 days per slot. |
| NFR-08 | The dashboard shall load in a browser with no build step โ no bundler or compilation required. |
6. Constraints¶
- Hosting platform: Azure only โ all infrastructure must run on Microsoft Azure.
- Budget: โค $30/month total (compute + database + storage combined).
- Network: The system is deployed inside the organisation's internal network or a private Azure-hosted container; it is not publicly accessible.
- Technology stack: Angular 21 for the frontend; .NET 10 for all backend components.
- Platform agnosticism: The solution must not depend on any proprietary cloud compute model (e.g. serverless Functions). All backend components must be deployable as standard containerised applications on any OCI-compliant container host.
7. Target Architecture¶
The project is a microservices architecture โ Write API, Read API, Fetcher (optional), Frontend SPA, and App Gateway are distinct services with distinct concerns, decomposed at the project + boundary level.
Microservices architecture. Write API, Read API, Fetcher, Frontend SPA, and App Gateway are distinct services with distinct concerns. Decomposition at the project + boundary level.
Separation of concerns (CQRS) consolidation in container write and read api separated as two different components but consolidated as container.
Passwordless Postgres auth (managed identity). For the Azure cloud target (NFR-01 / NFR-06), the stack auto-detects auth mode from credential presence: omit
POSTGRES_PASSWORDand the API authenticates as its ambient Azure Workload / Managed Identity, obtaining a short-lived access token at connection time refreshed transparently โ no static password in the environment. SetPOSTGRES_PASSWORDto use static credentials (default for local Compose, CI, and tests). The seam is provider-agnostic. See Configuration โ PostgreSQL: auth modes.
System Design¶
High-Level Overview¶
flowchart TD
subgraph GitHub["GitHub"]
CW["CI/CD Workflow<br/>(existing)"]
NS["Notify Step<br/>(new, ~5 lines)"]
end
CW -->|"deployment_status event"| NS
NS -->|"POST /api/deployments"| GW
subgraph Gateway["App Gateway (nginx) โ ONLY PUBLIC SURFACE"]
GW["routes per ยง7 Components<br/>App Gateway โ Routing matrix"]
end
subgraph Internal["Internal-only"]
FE["Dashboard Frontend<br/>(nginx + Angular static)"]
API["API (.NET 10)<br/>single host, three endpoint groups composed from libraries:<br/>โข Write โ POST โ INSERT+NOTIFY (API-key gated)<br/>โข Read โ matrix/history/discovery/SSE/LISTEN (unauthenticated)<br/>โข Control โ stream/events (mixed auth D8/D9)"]
PG[("PostgreSQL<br/>LISTEN/NOTIFY")]
end
GW --> FE
GW --> API
API --> PG
Clients["Browser + CI/CD"] -->|"one origin, no CORS"| GW
GW -.->|"via the gateway (internal-only)"| NC
NC["Notification Client<br/>(desktop tray, v2.0)<br/>Polls GET /api/deployments (via the gateway)<br/>OS notification on change<br/>Click โ open gateway URL"]
Optional pull-mode ingest edge โ When push-mode integration is not available, an opt-in fetcher component polls a CI/CD tool's API and pushes events through the same gateway like any other pusher:
flowchart TD
subgraph Optional["Optional โ when push-mode integration is not available"]
API["CI/CD Tool API<br/>(e.g. GitHub)"]
Fetcher["Dashboard.Fetcher.Host<br/>(separate container, opt-in)<br/>โข Polls CI/CD API<br/>โข POSTs /api/deployments with X-Progress-Reporter: dashboard-fetcher/<adapter><br/>โข GET/PUT /api/fetcher/state"]
end
Fetcher -->|"poll CI/CD API on interval"| API
Fetcher -->|"same X-Api-Key"| GW["gw"]
Fetcher -->|"for opaque cursor"| GW
C4 Component Diagram¶
The diagram below shows the logical components.
Expressed as a flowchart with a
subgraphboundary standing in for the C4 system boundary; external systems sit outside it.
flowchart TD
GHA["GitHub Actions<br/>[External System]"]
subgraph System["Deployment Dashboard System"]
Ingest["Ingest API"]
Store[("Deployment Store")]
Hub["Real-time Hub"]
ReadAPI["Read API"]
FE["Dashboard Frontend<br/>(browser)"]
end
NC["Notification Client<br/>(desktop tray)"]
GHA -->|"deployment event"| Ingest
Ingest -->|"persists"| Store
Ingest -->|"broadcasts"| Hub
ReadAPI -->|"queries"| Store
ReadAPI -->|"REST"| FE
Hub -->|"pushes events / live updates"| FE
NC -->|"polls REST"| ReadAPI
CI/CD Notify Step¶
A pipeline step that pushes deployment state to the API (see Summary row for context).
Dashboard Backend โ Write, Read, and Control API services (co-located)¶
Write, Read, and Control are distinct microservices with distinct concerns, co-located in one stateless ASP.NET Core container.
| Attribute | Value |
|---|---|
| Language | C# / .NET 10 |
| Framework | ASP.NET Core Minimal API |
| ORM | EF Core 10 + Npgsql |
| Storage | PostgreSQL (production and local dev); SQLite in-memory (unit tests only) |
| Scalability | Horizontal โ stateless; multiple instances behind a load balancer; the host scales as a whole today (surfaces co-located, independently scalable only after a future split) |
| Container | All C4 containers are running as docker containers |
Statelessness constraints (required for horizontal scaling):
| Concern | How the API host satisfies it |
|---|---|
| Caching | No in-memory cache of deployment state between requests โ every read hits the database. |
| Real-time fan-out | No in-process fan-out across instances โ events brokered via PostgreSQL LISTEN/NOTIFY. Each API instance independently LISTENs on the deployment_events channel (browser SSE) and control_events channel (component control stream), forwarding events to its own connected clients. |
| Session affinity | No sticky sessions โ load balancer may route any request to any instance. SSE connections are long-lived but reconnect transparently via Last-Event-ID. |
Responsibilities:
- Write surface โ accept and persist deployment events; NOTIFY the PostgreSQL
deployment_eventschannel on every successful ingest. - Read surface โ serve the unique services and feeds of events per service.
- Read surface โ stream real-time deployment slot-update events to browser clients via SSE (
LISTEN deployment_events). - Control surface โ orchestrate internal components; emit events on
GET /api/control/streamviaNOTIFY control_events; accept component status reports atPOST /api/control/events; list events atGET /api/control/events.
Why a gateway (vs. CORS + multiple origins): - Eliminates CORS entirely โ the browser only ever sees one origin. - Minimises the public surface โ only one container in NFR-04's internal-only network has ingress. - One ACA app gets public ingress in Azure; the others stay internal. - The SPA and CI/CD callers are upstream-agnostic โ they hit one URL.
Dashboard Frontend (MVP)¶
Angular 20 SPA in its own nginx container; reached only via the App Gateway. Attributes below.
6 box states:
Each slot resolves to one of six states based on the slot's wire shape:
| State | Condition | Box appearance |
|---|---|---|
| Success | Last deployment succeeded | Full green box โ version + actor + time |
| Running + Last Successful | Deploying now; previous terminal was success | Top: orange spinner + version; bottom: last successful version |
| Running + Failed + Last Successful | Deploying now; previous terminal was failure; an older success exists | Top: orange spinner + โ prev. failed badge; bottom: last successful version |
| Failed + Last Successful | Last deployment failed; an older success exists | Top: red failed + version; bottom: last successful version |
| Running | Deploying now; no prior successful deployment | Full orange spinning box โ version only |
| Running + Failed | Deploying now; previous terminal was failure; no successful history | Top: orange spinner + โ prev. failed badge; no bottom section |
The box is split into two sections by a dashed divider when a last-successful state differs from the current state. This makes it immediately visible what is running now versus what last worked.
Boxes share a version highlight on hover โ hovering a version amber-highlights all boxes across environments where the same version is deployed, making it easy to trace promotion progress.
Box shows all attributes (except synthetic) belongs to deployment event model, with ability to configure by user set of attributes shown.
Views¶
- Graph of deployments placed one under another, consolidating graphs of Github workflows for different services to one view.
- Services ร environments deployment matrix.
CI/CD Integration¶
The ingest API is the sole integration point. Any CI/CD tool that can make an HTTP POST request can send deployment events โ no CI/CD-specific SDK, plugin, or webhook infrastructure is required. The dashboard has no dependency on any particular build system and does not query any CI/CD tool.
Domain Model¶
Deployment event¶
| Attribute | Type | Required | Description |
|---|---|---|---|
id |
UUID (v7) | TRUE | Server-assigned surrogate row identifier. Time-ordered UUIDv7 โ unique and sortable by insert time (doubles as the SSE Last-Event-ID resume cursor). |
deployment_id |
STRING | TRUE | Emitter-supplied correlation key grouping all event rows of one logical deployment. NOT unique per row, NOT an idempotency key. |
service |
STRING | TRUE | Service (component / application) identifier. Wire name is service. |
environment |
STRING | TRUE | Environment identifier |
version |
STRING | FALSE | Version of service |
status |
ENUM | TRUE | in-progress / success / failure |
run_url |
STRING | FALSE | Link to the CI/CD run |
sha |
STRING | FALSE | Opaque commit SHA (not parsed) |
run_number |
INTEGER | FALSE | Numeric CI/CD run identifier |
ref |
STRING | FALSE | Opaque git ref (branch, tag, PR-42, refs/โฆ) |
actor |
STRING | FALSE | Username that triggered the run |
happened_at |
DATETIME | TRUE | Emitter-supplied UTC wall-clock at which the deployment transitioned to status. All read surfaces order by this value. |
parent_deployments |
STRING[] | FALSE | Explicit upstream correlation keys (each a deployment_id value), stored verbatim for client-side DAG rendering. |
Retention¶
Old rows are pruned by a background job. The retention window is configurable via the HISTORY_RETENTION_DAYS environment variable (default: 365).
The pruning job runs once per day and deletes rows where happened_at < NOW() - HISTORY_RETENTION_DAYS days.