Technical enablement · 04
Operations & administration
What the team that runs {{product.name}} needs: managing tenants and access, the scheduled jobs, reading system state,
handling the common failure modes, migrations, secrets and cost. Most of this page distils RUNBOOK.md in the
product repository; that file remains the detailed reference.
Operator-only. Administration can see across tenants. It is limited to platform_admin,
and infrastructure access is held by whoever has admin access to the Azure resource group. Do not expose any of this to client roles.
Tenancy & access management
| Task | How | Who | Audited |
|---|---|---|---|
| Create the first platform admin | make seed-admin EMAIL=<address> NAME="<name>" | Whoever holds the database credentials | No. The script writes the user directly and does not add an audit_log entry; record it in the engagement notes. |
| Create an engagement (tenant) | Engagements → new | platform_admin, capability_owner | Yes (audit_log) |
| Create a user and assign a role | Admin page | platform_admin | Yes |
| Edit or delete engagements, workloads, connections | Engagements → manage | platform_admin | Yes |
| Review sign-ins | Admin → login audit log | platform_admin | n/a |
| Break-glass access to a tenant | No procedure is documented in the RUNBOOK. Proposed: use the audited Engagements → manage view first; direct database access only with a recorded reason, a second approver and a time limit. | platform_admin; resource-group admin for database access | App actions yes (audit_log). Direct database access no, so log it by hand. |
Tenants carry a tier (diagnostic, tier_1, tier_2, tier_3) and a data_region.
The role list is on the feature reference.
Monitoring & health
| Signal | Where to look | Notes |
|---|---|---|
| Process liveness | GET /api/health | Public. Returns status and a timestamp only; it does not check the database or Redis. A deploy smoke check uses the sign-in page instead. |
| App logs | make logs | Streams Container App logs. Search [litellm] for gateway key warnings. |
| Scheduled job health | make cron-logs | Lists recent executions. A non-zero exit means a failure; a 200 from sync-billing alone does not mean it worked. |
| Connection health | Connections page | Status (disconnected, active, error), last sync, granted scope and the provider's own error message. |
| Checkpoint and escalation queues | /checkpoints, /escalations | Anything in_progress well past its target, or an escalation near its SLA, is worth a manual look. |
| Alerting and on-call | None configured. Proposed starting set: Container Apps alerts for restarts and a failed revision, an alert on a non-zero scheduled-job execution, an external check on the sign-in page, and database and cache resource alerts, sent to one named channel. On-call: business-hours, best effort, until a rota is agreed. | No alerting configuration is documented in the repository. |
Reading system state
- Zones (Agentic). GREEN is normal; AMBER needs a human decision soon; RED is already paused, stopped or refused at the gateway.
- CPVO states (Attribute).
OK,LOW_CONFIDENCE,COVERAGE_CAVEAT,RECONCILIATION_PENDING,NOT_ATTRIBUTABLEandNO_OUTCOMES. Calculation runs are immutable; a recompute always creates a new row. - Coverage badge (Govern). Percentage of AI spend the platform can see and reconcile against a named source.
- The inferred marker. A figure carrying it is inferred rather than measured. It is deliberately never a dismissible modal.
- Demo badge. Tenants flagged as demo show a DEMO pill in the top bar.
Scheduled jobs & data operations
A Container Apps Job, ca-ai-yield-cron, runs the app's own image every four hours (UTC 0 */4 * * *). It calls
three routes in order over HTTP so they execute inside the web app's process.
| Job | Route | Safe to re-run? | What it does |
|---|---|---|---|
| sync-billing | /api/jobs/sync-billing | Yes (idempotent upsert on the natural key) | Pulls a 72-hour window from every eligible connection. |
| reconcile | /api/jobs/reconcile | Yes | Compares real-time with billing and classifies discrepancies. |
| recompute | /api/jobs/recompute | Yes (adds new runs; never edits) | Writes CPVO, yield and AYCI runs. |
- Cadence is set by the billing APIs. Azure Cost Management refreshes actual cost only every few hours and each run spends two rate-limited cost queries per Azure connection. The 72-hour lookback covers restatement, so correctness does not depend on frequency. Override with
CRON_SCHEDULE. - Authentication. The routes need the
x-cron-secretheader matchingCRON_SECRET. A 401 means the secret does not match; a 503 meansCRON_SECRETis not set on the deployment at all. - Session check exemption. Job routes are exempt from the session check because a scheduler cannot present a cookie. If they were moved back under it, every scheduled run would be redirected to sign-in. This is invisible locally, where
AUTH_BYPASSskips the check, and is covered bytests/domain/proxy-auth.test.ts. - Request ceiling. The sync runs inside an HTTP request that ingress ends at 240 seconds. Retry budgets are capped (background retries at 90 seconds, best-effort token metrics at 15 seconds) and connections are processed one at a time with a deadline. Remaining connections are deferred to the next run. If sync-billing starts returning 504, suspect a retry budget before the provider.
- Rate limits are normal. A 429 from a billing API is expected. If a scheduled sync still reports 429 after retrying, wait for the next run rather than triggering more.
- Demo tenants are excluded from sync, because their connections are seeded props.
- Errored connections are retried; disconnected ones are not.
- Same secret on both. The app and the job must share
CRON_SECRET. After rotating, run bothmake deployandmake cron-deploy.
Destructive operations (deleting engagements, reseeding a non-local database) need care. make seed-demo against a non-local
database requires CONFIRM_PROD=1 on purpose, and recreates each demo tenant from scratch.
Common failure modes
Gateway traffic arriving with no workload
Events from keys that name no workload, or a workload the tenant does not hold, are stored rather than lost, with the key's project identifier retained. Attribute → Workloads lists them, biggest estimated spend first; naming one writes a mapping and backfills the stored events. Resolution order: the key's own workload id (if valid and held by the tenant), then the stored map, then unattributed. A key with no project identifier cannot be adopted; the fix is to set one on the key in the gateway. An event with no tenant is dropped at the gateway, so look there, not in the app.
Provider connections not syncing
- A connection is created disconnected. Verification on save, or the Test connection button, promotes it to active.
- Check the registered adapters in
lib/tdal/adapters/registry.ts; a provider absent there stays disconnected by design. - Test connection reports which Azure strategy was used (
auth:managed-identity,auth:service-principalorauth:static-token). Check that first when a connection breaks after a redeploy. - Rotating
CREDENTIAL_ENCRYPTION_KEYmakes every stored credential unreadable; they must be re-entered.
Estimated cost (Vertex)
Google publishes no actual-spend API; it exists only in a billing export configured per billing account. Where none is available the
adapter prices usage from the public catalogue and marks rows cost_basis = 'estimated'. An estimate is an upper bound: it sees
no committed-use discount or credit and cannot be reconciled to an invoice. A model is priced only where exactly one catalogue entry
matches, so ambiguous models report usage without cost. If an export becomes available, add it to the connection scope and billed rows
supersede estimated ones.
Sign-in failures
A bad tenant id shows as an issuer that will not resolve; a wrong AUTH_URL or missing AUTH_TRUST_HOST shows as a
redirect-URI mismatch from Entra. If every page 404s or 500s locally with AUTH_BYPASS, check the development session still has a valid identifier.
Database migrations
- The schema lives in
lib/db/schema.ts; migrations inlib/db/migrations/.make db-pushapplies the schema directly and is fine for early-stage or development use; for data that matters, generate and review a migration (npm run db:generatethendb:migrate). token_cost_periodis partitioned by month. New partitions are added by migration, notdb:push.- Changes that
db:pushcannot apply to a populated table (for example a unique index on a column whose rows all hold the default) ship as scripts inscripts/migrate-*.ts. They are idempotent and run once per environment:DATABASE_URL=... npx tsx scripts/<name>.ts. - Ordering rule. An additive migration leads the deploy: apply it first, then release, because the new code reads the new columns and the pages that do return 500 without them. The dangerous order is the opposite one, where a constraint is relaxed ahead of the code that handles the relaxed case.
Releasing and secret rotation
Release flow is on the deployment page. For secrets:
-
Update the value in your local
.envNever in the repository.
-
Run
make deployIt re-syncs the app's secrets and forces a new revision.
make releasealso rebuilds and pushes. -
If it is
CRON_SECRET, runmake cron-deploytooUpdating only one side leaves the scheduler receiving 401s.
Cost & usage
The platform reads provider billing data, and some reads are themselves chargeable. Agree reporting with Finance; this section covers the technical mechanism only.
| Cost driver | Why | Control |
|---|---|---|
| AWS Cost Explorer reads | AWS charges per Cost Explorer API request, and a busy account paginates. Verifying a connection makes the same calls. | Sync frequency is a cost decision: daily is enough where the data settles daily. Avoid repeatedly pressing Test connection. |
| Azure Cost Management reads | Heavily rate-limited rather than charged; two queries per connection per run. | Four-hourly cadence; bounded retries. |
| Compute, database and cache | Container Apps, PostgreSQL and Redis for the platform itself. | Proposed: tag every platform resource with a cost code and watch it in Azure Cost Management, kept separate from the client spend the platform reads. |
Support & escalation
- Data privacy, GDPR and regulated-data questions go to the Data Privacy Officer.
- Legal, compliance and financial questions go to those functions. The product and its assistants do not give that advice.
- Infrastructure and deploy issues go to whoever holds admin access to the resource group.
- Support channel: {{links.support}} · Contact: {{product.contactEmail}}
- Severity and response are not yet agreed. Proposed model, to be confirmed by the service owner: Severity 1, the service is down or client-mode data may have leaked: respond within one working hour, update every two hours. Severity 2, a feature or a provider sync is failing for one or more engagements: respond the same working day, update daily. Severity 3, a defect with a workaround or a question: respond within three working days.
Runbook index
The repository has one living runbook, RUNBOOK.md, rather than separate numbered runbooks. Its sections:
| Section | Covers |
|---|---|
| 1–3 | Architecture at a glance, daily operation, reading system state. |
| 4 | Failure modes: unattributed gateway traffic, sync-billing, scheduled jobs, estimated cost, AWS and Azure credentials, connections, Entra, rollback, migration drift. |
| 5 | Releasing with GitHub Actions and the one-time identity setup. |
| 6 | Secret rotation. |
| 7–8 | Escalation contacts and the three demo tenants. |
| 9 | Explicitly out of scope: an end-to-end test suite, a formal security review write-up and an accessibility audit. |
Related: non-functional targets and deployment.