Technical enablement · 01
Architecture overview
How {{product.name}} is put together: the system context, its main components, where data lives, how it connects to other systems, and the security and non-functional properties an architect needs to assess. Start here, then drill into the components catalogue and deployment.
Sources. This page is derived from the product repository: README.md,
RUNBOOK.md, app-build-brief/03_ARCHITECTURE.md, app-build-brief/04_DATA_LAYER_TDAL.md
and the ADRs in docs/adr/. Items marked design target come from the build brief and have not
been measured in production.
Executive summary
{{product.name}} measures, governs and optimises the value delivered by AI investments across client engagements. It is a single Next.js 16 App Router application (TypeScript) that serves both the user interface and the API, backed by PostgreSQL for durable state, Redis for hot state and replay protection, and LiteLLM as the gateway through which AI traffic is observed. The decisions that most shape the design are: calculations are pure server-side functions; protected figures are removed by a server-side projection, never hidden in the browser; and money comes only from the reconciled billing stream.
| Attribute | Summary |
|---|---|
| Architectural style | One deployable application (UI and API together), layered: presentation, domain, data access, provider data layer. Provider ingestion runs as scheduled job routes plus a webhook receiver; there is no separate worker fleet. |
| Primary runtime | One container on Azure Container Apps, plus a scheduled Container Apps Job that runs the same image. Live |
| Tenancy model | Shared cluster, every row scoped by tenant_id at the repository layer. Dedicated deployment per tenant is a configuration of the same codebase, not a fork (design). |
| Key technologies | Next.js 16, React 19, TypeScript, Tailwind CSS 4, Drizzle ORM, PostgreSQL, Auth.js v5 (Microsoft Entra ID), ioredis, Recharts, Zod, Vitest, Playwright (declared; no E2E suite yet). |
| Source of truth | The product repository, the ADR index in docs/adr/ and RUNBOOK.md. The repository sits in an internal GitHub organisation; ask the product team for access. The link is not published on this page. |
System context
The context diagram shows {{product.name}} as a single box with the people and systems around it. Capgemini staff and client users use the same application; what each may see is decided server-side (see security and tenancy).
Components
The product is one deployable plus one scheduled job. Per-component detail is in the components catalogue; this table is the map.
| Component | Responsibility | Runtime | Source path |
|---|---|---|---|
| Web application | Server components and client components for the internal and tenant workspaces. | Next.js 16 App Router, React 19 | app/, components/ |
| API route handlers | Ingest webhooks, Gate A, checkpoints, escalations, optimisation, exports, job triggers, health. | Next.js route handlers (same container) | app/api/ |
| Domain library | Pure calculations: CPVO, Verified Yield, AYCI, zones, confidence, checkpoint options. | TypeScript, no I/O | lib/domain/ |
| Mode projector | The single place that decides what a role and mode may see. | TypeScript, server-only | lib/ip-mode/ |
| Token data abstraction layer (TDAL) | Provider adapters, normaliser, reconciler and completeness score writing the two token streams. | TypeScript, server-only | lib/tdal/ |
| Repositories | Tenant-scoped data access over Drizzle. | TypeScript | lib/db/repositories/ |
| Scheduled job runner | Calls sync-billing, reconcile and recompute every four hours. | Container Apps Job, same image, different entrypoint | scripts/run-jobs.mjs |
| LiteLLM gateway | Routes model calls and fires telemetry callbacks. Separate container; not part of this repository. | LiteLLM proxy | External (ADR 001) |
Layering rules
-
Domain code is pure
Calculation modules in
lib/domain/take inputs and a parameter set and return results with provenance. They hold no database or network access, which keeps them unit-testable and out of any client bundle. -
Everything leaves through the projector
Route handlers and server components pass domain results through
lib/ip-mode/project.tsbefore they reach a client. A field added to a domain type stays invisible in client mode until someone deliberately adds it (ADR 004). -
Repositories are tenant-scoped
Queries take an explicit tenant context. There is no ambient tenant, and every row carries a
tenant_id.
Request lifecycle
Two paths matter: the real-time path that observes AI traffic, and the billing path that produces the authoritative money figures.
Real-time path (estimated cost)
-
Model call passes through LiteLLM
An application or agent calls the gateway. After the call, LiteLLM fires an asynchronous callback; it never blocks the model response.
-
Callback is validated and stored
POST /api/ingest/litellmverifies the signature, rejects replays using a Redis nonce, resolves tenant, workload and session, and writes a row totoken_event_rt. Events are written one at a time, so a single bad event cannot discard a batch. -
Zone evaluated, change published
Session counters are updated in Redis, the zone is evaluated by a pure function, and a zone change is published to a Redis channel that feeds the browser over Server-Sent Events (ADR 003).
Billing path (reconciled, authoritative)
-
Sync
/api/jobs/sync-billingpulls a 72-hour window from every eligible provider connection and upsertstoken_cost_periodon its natural key, so re-runs are safe. -
Reconcile
/api/jobs/reconcilecompares real-time aggregates with billing per workload and period, classifies discrepancies and recomputes the coverage score. -
Recompute
/api/jobs/recomputewrites new, immutablecalculation_runrows for CPVO, Verified Yield and AYCI. Nothing is edited in place.
Data tiers
The two token tables are structurally separate by design (ADR 006). The real-time table is never a source for a money figure.
| Tier | Purpose | Technology | Retention / notes |
|---|---|---|---|
Authoritative billing (token_cost_period) | Reconciled provider cost; the only input to CPVO and financial reporting. | PostgreSQL 16, partitioned by month; FOCUS 1.5 field names | Retained indefinitely. Money stored as bigint minor units, never floating point. |
Real-time estimate (token_event_rt) | Per-call telemetry for session monitoring, retry and bypass detection, and shared-scope apportionment. | PostgreSQL 16, partitioned by day (ADR 002) | Disposable; 90 days by default. Cost is tagged as estimated. |
Audit spine (calculation_run, audit_log, gate_a_decision) | Append-only record of computed figures, mutations and Gate A decisions. | PostgreSQL | No updates in place; a recompute adds a row with a recorded reason. |
| Cache / pub-sub | Session counters, zone state, SSE fan-out, replay-protection nonces. | Redis (REDIS_URL) | Non-durable; rebuildable. |
| Secrets | App secrets as Container App secrets; connection credentials encrypted at rest in the database. | Container Apps secrets; AES-256-GCM for stored credentials | Rotating the encryption key makes stored credentials unreadable; they must be re-entered. |
No prompt or completion content is stored (ADR 005). The platform keeps token counts, model identifiers, costs, session identifiers and workload attribution only. That keeps content-level data protection out of scope for onboarding a client. Platform user identities and outcome-event metadata remain; involve the Data Privacy Officer before go-live, and do not paste client data into these pages.
Integration points
Every interface that crosses the product boundary. Detail on adding or swapping providers is in Integrations.
| Interface | Direction | Protocol | Auth | Failure behaviour |
|---|---|---|---|---|
| Microsoft Entra ID | Outbound | OIDC | Client credentials held as app secrets | Sign-in fails; existing sessions are unaffected until expiry. |
LiteLLM callback (/api/ingest/litellm) | Inbound | HTTPS webhook | HMAC signature over the body inside a 30-second window; the signature is held as a replay nonce | Rejected requests are not stored; events without a tenant are dropped at the gateway. |
Gate A (/api/gate-a) | Inbound | HTTPS / JSON | Bearer token | Every decision, including refusals, writes an immutable record. |
Outcome ingest (/api/ingest/outcomes) | Inbound | HTTPS / JSON | Machine-authenticated ingest path (no session cookie) | Idempotent on a client-supplied event key. |
| Provider billing and metrics APIs | Outbound | HTTPS (Azure Cost Management, AWS Cost Explorer and CloudWatch, Google BigQuery, OpenAI and Anthropic admin APIs) | Read-only credentials per connection | Rate limits are retried inside a bounded budget; a connection in error is retried on the next run. |
Job triggers (/api/jobs/*) | Inbound | HTTPS | x-cron-secret header | 401 if the secret does not match; 503 if CRON_SECRET is unset. |
| Live monitor stream | Outbound to browser | Server-Sent Events | Session cookie | Browser reconnects automatically; connection state is shown in the UI. |
Security & tenancy
Orientation only, not a compliance statement. Involve Information Security for approvals.
| Concern | Approach |
|---|---|
| Authentication | Microsoft Entra ID through Auth.js v5 for staff. A proxy (proxy.ts) applies a session check to every route except the sign-in, auth and health routes, and the machine-authenticated paths (/api/jobs, /api/ingest, /api/gate-a), which carry their own credential and fail closed without it. AUTH_BYPASS is a local development switch and must never be set in a deployed environment. |
| Authorisation | Eight roles: platform_admin, capability_owner, expert, consultant, commercial (internal) and client_admin, client_viewer, client_operator (client). See roles. |
| Dual-mode IP projection | Every tenant page can render in internal or client mode. Client roles are locked to client mode. Projection is a server-side allow-list transformation (ADR 004); client-side hiding is treated as a defect. A leakage test runs in CI and covers routes, endpoints, event frames and exports. |
| Tenant isolation | Tenant identifier on every row and bound at the repository layer. Design intent: a tenant mismatch returns 404, not 403, so tenant existence cannot be enumerated. ADR 002 and the build brief describe row-level security as a second line of defence, but no row-level security policies are defined in the repository's schema or migration scripts. Today, isolation rests on the application-level scoping above; database-enforced row-level security is a design target, not a delivered control. |
| Encryption | TLS in transit. Stored provider credentials are encrypted with AES-256-GCM using CREDENTIAL_ENCRYPTION_KEY. The build brief requires encryption at rest for managed PostgreSQL and Redis; that is a property of the Azure service configuration rather than of the application code, and this page does not assert it for the live estate. CREDENTIAL_ENCRYPTION_KEY is held as a Container App secret. Rotating it makes every stored credential unreadable, so credentials must be re-entered afterwards. |
| Secrets | Held as Container App secrets and referenced by environment variable; never committed. Connection credentials are either encrypted values or references (env:, keyvault:, managed-identity:, azure-default). |
| Audit logging | Mutations are recorded in audit_log; login events appear in the admin audit view; Gate A decisions and checkpoint decisions are immutable records. |
| Supply chain | CI runs typecheck, tests and a production build on every pull request; lint is also blocking. The deploy pipeline signs in to Azure by OIDC federation, so no cloud client secret is stored in GitHub, and the deploy identity can push images and update only the app and its scheduled job. No dependency scanning, image signing or SBOM generation is configured in the repository's workflows. Proposed: enable GitHub dependency alerts and add an npm audit step to CI; signing and SBOM to be decided by the service owner. |
Protected calibration and threshold parameters never reach a client-mode payload. This page deliberately does not list them; the authoritative design is in ADR 004 and the IP-mode brief.
Non-functional requirements
The targets below are design targets from app-build-brief/03_ARCHITECTURE.md. They are not measured service levels.
| Quality | Target | How measured |
|---|---|---|
| Availability | No availability target has been agreed. Proposed target for the shared instance: 99.5% a month, excluding planned releases; to be confirmed by the service owner. | Proposed: an external uptime check on the sign-in page, which is what the deploy smoke check already uses. /api/health reports process liveness only. |
| Gate A decision | Under 200 ms p95 (design target; it sits in the dispatch path) | Not instrumented. No tracing or APM is configured in the repository. Proposed: log the route duration for Gate A and review the 95th percentile in Container App logs. |
| Zone change to browser | Under 2 s p95 (design target) | Not instrumented. Proposed: compare the zone write time with the time the stream frame is sent, both in the logs. |
| Billing sync, one connection, one day | Under 60 s (design target) | Per-connection outcomes in the sync response |
| Request ceiling | Container Apps ingress ends a request at 240 s; sync retry budgets are sized to fit inside it | See scheduled jobs |
| Recovery | No recovery objectives have been agreed. The build brief requires managed PostgreSQL with point-in-time recovery, so the achievable restore point depends on the backup tier set on the live server. Application recovery is redeploying a previous image (see rollback). Proposed targets: RPO 1 hour and RTO 4 hours; to be confirmed by the service owner. | No disaster-recovery exercise has been recorded. Proposed: one database restore test before the first real client is onboarded. |
| Accessibility | WCAG 2.2 AA is the build standard in the design brief (10_UX_AND_BRAND.md). Conformance has not been audited, and an accessibility audit is explicitly deferred (RUNBOOK section 9). | Not yet measured. Proposed: an automated accessibility check in CI plus a manual audit before first client go-live. |
| Cost | No unit-cost envelope has been set. Proposed: record the platform's monthly running cost for a full month, then express it per engagement and per connection. | See Operations: cost. |
Key decisions & open risks
ADR-001 LiteLLM as gateway substrate Accepted
ADR-002 Postgres, not a time-series database Accepted
ADR-003 Server-Sent Events, not WebSockets Accepted
ADR-004 Server-side mode projection Accepted
ADR-005 No prompt or completion content storage Accepted
ADR-006 TDAL unified schema lock (FOCUS 1.5) Locked
ADR-007 Azure Web App deployment Accepted (see note)
ADR-008 Govern module architecture Accepted
ADR-009 Attribute module: CPVO engine Accepted
ADR-010 Agentic governance: Gate A and Component 8 Accepted
ADR 007 versus what runs. ADR 007 records an Azure Web App (App Service) target. The deployed system runs on Azure Container Apps, using the same standalone container build. Treat the Container Apps description on the deployment page as current. No ADR records the move to Container Apps. Proposed: write ADR 011 to supersede the hosting section of ADR 007.
- Gateway bypass. Agents that call model SDKs directly sit outside Gate A coverage; this is a named gap surfaced on the Connections page (ADR 001).
- Revisit triggers. Postgres for real-time events is revisited if single-day partition queries exceed 200 ms p95 (ADR 002); LiteLLM is revisited at sustained high gateway volume for one tenant (ADR 001). Neither threshold has been observed. ADR 002 sets a numeric trigger (200 ms p95); ADR 001 deliberately leaves its threshold unset until one is seen, so none is recorded.
- Request-path sync. The billing sync runs inside an HTTP request. Persistent deferrals mean more connections than one run can service; the runbook's remedy is to split the schedule or move the sync out of the request path.
Next: the components catalogue and the deployment page.