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.

Layer 1 — Desktop host Layer 2 — Shared Azure substrate Layer 3 — Per-engagement

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.

💻
Layer 1

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)
Layer 1 is engagement-neutral. The same desktop install can serve any engagement; which customer it is working on is decided entirely by the Layer 3 configuration it loads, not by anything baked into the host.
☁️
Layer 2

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 typeRole in the system
Postgres flexible serverThe system of record. Hosts one schema per engagement; every write is hash-chained for tamper-evidence. The single source of truth.
Key VaultHolds 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 endpointEmbeddings 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 InsightsApplication telemetry for the Cardinal app and API — requests, dependencies, failures, and traces.
Log Analytics workspacePlatform-wide log retention and query, backing Application Insights and resource diagnostics.
Documents live in SharePoint — and never pass through Cardinal. Alongside the Azure resources above, every engagement's documents live in a shared SharePoint document library. Files are created and written directly to SharePoint — by people or by the customer — and are never uploaded through Cardinal for any reason. Cardinal only ever watches that library and records lightweight metadata about each file — its title, its location, and when it last changed. The body and bytes are never copied into the database or onto the desktop; a lossy embedding derived from each document is stored to power semantic search, and the original text is discarded immediately after it is computed. The contents stay in SharePoint.
Why a shared substrate? Standing up Postgres, Key Vault, storage and an embeddings endpoint once — and giving each engagement only a thin slice on top — keeps per-engagement onboarding fast and cheap, and keeps the security surface in one place.
🧩
Layer 3

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
🌐
The app

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) and mut.* (audited, idempotent)
  • Keyless database auth via a user-assigned managed identity
  • The Static Web App reaches it through a linked backend
Keyless by design. The API authenticates to Postgres with a managed identity and a short-lived token, then adopts a least-privilege database role per session. There is no database password anywhere in the app, the corpus, or the deployment pipeline.

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 App
  • func-<app>-api-<region> — the Function App
  • kv-<orchestrator>-<region> — the shared Key Vault
  • pg-<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.

Next: install Cardinal on your machine →