Install & infrastructure
Three layers, one system of record.
For: developers, reviewers, and architects
Cardinal runs across three clearly separated layers: a desktop AI host where the agents live, a shared Azure substrate that every engagement draws on, and a thin slice of per-engagement resources. This page is the reference catalogue: every resource type, what it is for, and how the layers connect.
The three-layer model
The clean separation is what keeps the system honest. Customer-specific detail exists only at Layer 3; the shared substrate (Layer 2) is engagement-agnostic; and Layer 1 — your laptop — holds no customer data at rest beyond what the agents are actively working on.
flowchart TB
subgraph L1 ["💻 LAYER 1 — Desktop AI host (your laptop)"]
direction LR
HOST["🤖 AI host
agent sessions"]:::local
CORPUS["📚 Corpus
~/.copilot"]:::local
BUS[("🗄️ Local Postgres
message bus")]:::local
MCP["🔌 MCP servers
DAB · ADO · …"]:::local
end
subgraph L2 ["☁️ LAYER 2 — Shared Azure substrate (once per programme)"]
direction LR
PG[("🐘 Postgres
flexible server")]:::azure
KV["🔐 Key Vault"]:::azure
ST["📦 Storage
corpus mirror"]:::azure
OAI["🧠 Azure OpenAI
embeddings"]:::azure
end
subgraph L3 ["🎯 LAYER 3 — Per-engagement slice (once per customer)"]
direction LR
SCHEMA[("📂 Engagement schema
+ 3 DB roles")]:::engagement
SECRETS["🗝️ Engagement
KV secrets"]:::engagement
end
subgraph APP ["🌐 THE CARDINAL APP — SWA + Functions"]
direction LR
SWA["🖥️ Static Web App
SPA"]:::app
FUNC["⚡ Functions
tRPC API"]:::app
end
L1 == managed identity · keyless ==> L2
L2 -. carves out .-> L3
APP == same SPs · same DB ==> L2
style L1 fill:#fdf5f7,stroke:#b11f4b,stroke-width:2px,color:#7a1132
style L2 fill:#f0f7ff,stroke:#0078d4,stroke-width:2px,color:#003966
style L3 fill:#f0fbf8,stroke:#0d9488,stroke-width:2px,color:#134e4a
style APP fill:#fbf7ff,stroke:#7c3aed,stroke-width:2px,color:#4c1d95
classDef local fill:#fce4ec,stroke:#b11f4b,stroke-width:2px,color:#7a1132
classDef azure fill:#dbeafe,stroke:#0078d4,stroke-width:2px,color:#003966
classDef engagement fill:#ccfbf1,stroke:#0d9488,stroke-width:2px,color:#134e4a
classDef app fill:#ede9fe,stroke:#7c3aed,stroke-width:2px,color:#4c1d95
Both halves — the agents on the desktop and the Cardinal web app — write through the same stored procedures into the same database. That shared write-path is the contract between them. See Data Model for the write discipline.
The desktop AI host
The operator's laptop runs the desktop AI host. This is where the long-lived agents live, where the skills / role-briefs / workflows corpus sits, and where a small local database backs inter-agent messaging. It holds no customer data at rest beyond what the agents are actively metabolising.
What runs here
- The AI host and its long-lived agent sessions
- The corpus: skills, role briefs, workflows, engagement packs
- A local Postgres instance backing the inter-agent message bus
- MCP servers (the database access bridge, work-item tracker, and others)
- An encrypted local cache for credentials and keys
How it survives a laptop loss
- The corpus is mirrored to Azure Blob storage (Layer 2) and restored on a new machine
- Blob versioning + a 14-day soft-delete window give point-in-time restore
- Scouts keep a per-source cursor, so a sleep or outage causes latency, not data loss
- Recovery is re-auth → restore corpus → re-spawn agents (see Operations)
The shared Azure substrate
A single set of shared Azure resources that every engagement draws on. Provisioned once for the programme; engagement-agnostic. Resources are referenced here by type and role — the concrete names are deployment configuration, not architecture.
| Resource type | Role in the system |
|---|---|
| Postgres flexible server | The system of record. Hosts one schema per engagement; every write is hash-chained for tamper-evidence. The single source of truth. |
| Key Vault | Holds the programme-level secrets and signing keys. Accessed by managed identity; no secret values ever live in the corpus or in code. |
| Storage account (Blob) | The corpus mirror — skills, role briefs, workflows, engagement packs — with versioning and soft-delete for recovery. Entra-authenticated; no anonymous access. |
| Azure OpenAI endpoint | Embeddings for the conversation-grouping / retrieval substrate. Keyless (managed-identity) access. No model fine-tuning; inference only. |
| Data API Builder (DAB) | Surfaces the database's write-path entities to the agents through a standard tool interface, enforcing the same stored-procedure discipline as the app. |
| Application Insights | Application telemetry for the Cardinal app and API — requests, dependencies, failures, and traces. |
| Log Analytics workspace | Platform-wide log retention and query, backing Application Insights and resource diagnostics. |
Per-engagement resources
The only layer that carries customer identity. When a new engagement is onboarded, a small, well-defined set of resources is provisioned on top of the shared substrate. Everything customer-specific — including, in these docs, the placeholder customer Fabrikam — lives here and nowhere else.
What gets provisioned
- A dedicated database schema for the engagement
- Three Postgres roles (read, app, owner) scoped to that schema
- A set of engagement-scoped Key Vault secrets (connection string, signing keys, integration credentials)
- A GENESIS audit row that seeds the tamper-evident chain
How it is created
- A single onboarding ceremony provisions it all (dry-run first, then an explicit apply)
- The schema's tables, stored procedures and triggers are replayed from canonical SQL
- Three independent gates must pass: a secret scan, a kernel-purity scan, and a config validator
- Only after the operator ratifies does the engagement flip from mobilising to live
Cardinal's own hosting
Cardinal — the web application — is hosted on its own Azure resources, separate from the shared substrate but writing into the same database. The front end and back end deploy independently.
Front end
- A React SPA (React 18) on Azure Static Web Apps
- TanStack Router (file-based) + TanStack Query; typed tRPC client
- ECharts for metrics; Tailwind + Radix UI; Vite build
- Auth via the Static Web Apps client principal; role-gated views
Back end
- A tRPC server on Azure Functions (Flex Consumption, Node)
- Routers split into
read.*(no side effects) andmut.*(audited, idempotent) - Keyless database auth via a user-assigned managed identity
- The Static Web App reaches it through a linked backend
How it ships
- The SPA is built and published to Static Web Apps
- The API is bundled and published to the Function App (Flex Consumption deploys via the platform's deploy API)
- A live smoke-probe confirms the deploy, the Steward attests the shipped change on the audit spine, and the roadmap is updated
Naming & regions
Azure resources follow a consistent, type-first naming pattern. In these public docs we describe resources by their type and role rather than their literal names — the names are deployment configuration, and an instance name carries no architectural meaning a reader needs.
The naming pattern
Resources follow a <type>-<app>-<region> shape, for example:
swa-<app>-<region>— the Static Web Appfunc-<app>-api-<region>— the Function Appkv-<orchestrator>-<region>— the shared Key Vaultpg-<programme>-shared-<region>— the Postgres server
In Bicep this is a single appName parameter interpolated into each name.
Regions
Resources are deployed to a primary region (for the reference deployment, UK South), chosen for data residency and proximity to the team. The architecture is region-portable: pick a region near your users and the naming pattern carries the region token through.