Technical enablement · 06

Deployment models

How {{product.name}} is built, shipped and run today on Azure Container Apps, how releases flow from GitHub Actions, which environment variables it needs (names only), and what a dedicated deployment would involve.

⚠

Commercial, legal and data-residency questions (pricing, liability, hosting location, subprocessors) are not decided on this page. Involve Legal, Compliance and the Data Privacy Officer early, and treat the technical options below as input to those conversations.

The two models at a glance

Model A · Shared platform instance Operated by the product team · live Engagement 1 Engagement 2 Engagement N Shared {{product.name}} on Azure Container Apps tenant scoping in the app and data layer one database, one cache, one gateway One release train · shared capacity · fastest onboarding Model B · Dedicated deployment Design intent · not yet exercised One regulated or region-bound client Same image, separate stack own database, cache and gateway in the tenant's declared data region Configuration of one codebase, never a fork
Figure 1 — The two models. Only Model A exists today. Model B is the design intent in the build brief (03_ARCHITECTURE.md, section 6).

Comparison

DimensionA · Shared platform instanceB · Dedicated deployment
StatusLiveRoadmap
Time to first valueAn engagement is created in the app and connections are added; no infrastructure work.No dedicated stack has been built, so there is no measured lead time. Planning assumption: several weeks, dominated by approvals (data protection, networking, identity) rather than build effort; to be confirmed by the service owner.
Data location & controlHosted by the product team. Each tenant has a data_region setting. The live application runs in the Azure North Europe region. The tenant setting is a recorded requirement that the application does not enforce, and it defaults to UK, so check where the database and cache sit before promising residency to a client.Database, cache and backups must all sit in the tenant's declared region.
Tenant isolationLogical (application and data layer).Separate stack.
UpgradesEvery commit to main deploys automatically after CI passes.Proposed: releases in an agreed change window with client approval, using the same image tag as the shared instance; to be agreed per client.
OperationsProduct team; see operations.Proposed: a written operating agreement before build, naming a technical owner, support hours, escalation route and who holds the secrets.
ⓘ

Other clouds. The connection design is host-portable (see Azure credentials), and work to run a second instance on AWS has started, but it is not live and nothing on this page should be read as describing it. Status: planning and scaffolding only. The plan is containers on AWS ECS Fargate behind a load balancer in eu-west-2, and it is waiting on a networking request to the host organisation's IT group. No target date has been set.

Model A — the Azure Container Apps instance

Runtime topology

PieceWhat it is
Web appAzure Container App ca-ai-yield: the Next.js standalone server, port 3000, behind Container Apps ingress (240-second request ceiling).
Scheduled jobAzure Container Apps Job ca-ai-yield-cron in the same environment, schedule 0 */4 * * * (UTC), running the same image with the entrypoint node /app/scripts/run-jobs.mjs.
Image storeAzure Container Registry. Images are tagged with the short git commit SHA.
DatabasePostgreSQL, single schema ai-yield, reached through DATABASE_URL.
CacheRedis, reached through REDIS_URL over TLS.
GatewayLiteLLM in a separate container; not deployed from this repository.
IdentityMicrosoft Entra ID for sign-in; a managed identity for reading Azure billing; a separate user-assigned identity for the deploy pipeline.
ⓘ

Resource-group, subscription and tenant identifiers are deliberately not published here. The resource reference is kept in the product repository (RUNBOOK.md section 1 and the configuration block at the top of the Makefile); subscription and tenant identifiers are held as repository secrets and are available from the platform admin.

Environment variables

Names only. The values live in a local .env (never committed) and, once deployed, as Container App secrets. .env.sample in the repository lists the same names.

VariablePurposeHeld as
DATABASE_URLPostgreSQL connection.Secret
REDIS_URLRedis connection; needed for gateway replay protection and the live stream.Secret
AUTH_SECRETAuth.js session signing.Secret
ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET, ENTRA_TENANT_IDEntra ID app registration for staff sign-in.Secrets
AUTH_URL, AUTH_TRUST_HOSTPublic base URL and proxy trust for Auth.js.Plain
CRON_SECRETAuthenticates the scheduled job to the job routes (x-cron-secret).Secret on the app and on the job
CREDENTIAL_ENCRYPTION_KEYEncrypts provider credentials entered in the app.Secret
LITELLM_CALLBACK_SECRETVerifies gateway callbacks; must match the gateway's value.Secret
AZURE_MI_CLIENT_IDSelects the user-assigned managed identity used to read Azure billing.Plain
AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRETService-principal fallback for Azure connections outside Azure.As needed
AZURE_ARM_TOKENShort-lived token for local Azure testing only.Local .env only
JOBS_BASE_URL, JOBS_LOOKBACK_HOURSWhere the job runner calls, and the re-pull window (default 72 hours).Plain, on the job
DEPLOYED_ATSet to a timestamp on every deploy so a new revision always rolls.Plain
AUTH_BYPASSLocal development only. Never set in a deployed environment.Local .env only
❗

Never commit variable values, and never paste them into these pages. If Redis is unreachable or REDIS_URL is missing, gateway callbacks cannot be accepted; make deploy stops before deploying without it.

Make targets

The Makefile reads .env and wraps the Azure CLI. Defaults can be overridden on the command line.

TargetWhat it does
make devStart the local dev server.
make testTypecheck plus the unit tests.
make db-push / db-studioApply the schema directly, or open Drizzle Studio.
make seed-adminCreate a platform_admin user (EMAIL=, NAME=).
make seed-demoSeed the three demo tenants (ONLY=, CONFIRM_PROD=1).
make buildBuild the image for linux/amd64, tagged with the commit SHA.
make pushLog in to the registry, build, push and deploy.
make deploySync the app's secrets and environment, then update the Container App image. TAG= deploys a specific image.
make releasemake test, then make push.
make cron-deploy / cron-run / cron-logsCreate or update the scheduled job (idempotent), run it once now, list recent executions.
make logsStream live Container App logs.
make jobs-localRun the same job runner against a local dev server.
make arm-token / aws-credsRefresh short-lived cloud credentials in .env for local connection testing.
⚠

Uncommitted changes redeploy the old image. The tag is the commit SHA, so deploying with uncommitted changes leaves the tag unchanged. Commit first. Also, make deploy on its own does not update the scheduled job image; the pipeline does.

CI/CD with GitHub Actions

Two workflows in .github/workflows/.

WorkflowTriggerWhat it does
ci.ymlEvery pull request; reused by the deploy workflowTypecheck, the full Vitest suite (leakage, tenancy and redaction tests included) and a production build on Node 20; lint runs as a blocking job. Needs no secrets.
deploy.ymlEvery commit to main; manual dispatchRuns CI as its gate, builds and pushes the image tagged with the short commit SHA, updates the Container App, rolls the scheduled job to the same image, then smoke-checks that the new revision serves.
  • Sign-in by OIDC federation. The deploy job signs in to Azure with a federated token, so no client secret is stored in GitHub. The federated credential is bound to the production environment, not to a branch. The GitHub secrets hold identifiers only (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID).
  • Least privilege. The pipeline identity can push an image and roll a revision on the two named resources. It cannot widen its own access.
  • App secrets stay out of GitHub. They already live on the Container App and survive an image update. To add or rotate one, run make deploy from a machine holding .env.
  • Gating. Add required reviewers to the production environment to make deployment an approval step rather than automatic.
  • Smoke check. The pipeline polls the sign-in page for a 200 for up to two minutes.
ⓘ

If a workflow run never appears, check that the repository's Actions policy allows GitHub-owned actions. When it does not, no run is created at all, which reads as "CI is broken". The RUNBOOK (section 5) has the check.

Rollback

Deploy the previous image tag. Find tags in the registry and the running revision in the Container App.

Roll back to a previous image
make deploy TAG=<previous-tag>

# or directly:
az containerapp update --name ca-ai-yield --resource-group <RESOURCE_GROUP> \
  --image <REGISTRY>/ai-yield:<previous-tag>

Database changes do not roll back with the image. Additive migrations are safe to leave in place; see migrations for the ordering rule.

Model B — dedicated deployment

Roadmap

The build brief intends a dedicated deployment to be a configuration of the same codebase, used where a regulated client needs isolation or a specific data region. No dedicated instance has been built, and no infrastructure-as-code for one exists in the repository. Working assumption, not a decision: the same image on Azure Container Apps in the client's declared region, run by the product team. No owner or decision date has been set; the decision belongs with the service owner, Legal and the Data Privacy Officer. The AWS work is a separate track for a second instance, not a dedicated client stack.

Prerequisites a dedicated stack would need

  • PostgreSQL and Redis in the client's declared region, with the application in the same region.
  • A container platform able to run the image and a Container Apps Job equivalent (or any scheduler that can call the three job routes with the x-cron-secret header).
  • Its own LiteLLM gateway, with a callback secret shared with the app.
  • An Entra ID app registration, or an agreed alternative sign-in route. Client-user sign-in is not designed beyond Entra ID today. The brief mentions per-tenant identity providers or email one-time codes, neither of which is built. Proposed first step: invite client users as guests to the product's Entra directory, which needs no code change; to be confirmed.
  • A named technical owner and an agreed change window.

Which model to choose

If the engagement...Lean towards
can start with read-only provider credentials and a shared platformModel A (available now)
has strict data-residency or isolation requirementsModel B is the design answer, but it is not built. Raise with the product team, Legal and the Data Privacy Officer.