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
03_ARCHITECTURE.md, section 6).Comparison
| Dimension | A · Shared platform instance | B · Dedicated deployment |
|---|---|---|
| Status | Live | Roadmap |
| Time to first value | An 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 & control | Hosted 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 isolation | Logical (application and data layer). | Separate stack. |
| Upgrades | Every 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. |
| Operations | Product 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
| Piece | What it is |
|---|---|
| Web app | Azure Container App ca-ai-yield: the Next.js standalone server, port 3000, behind Container Apps ingress (240-second request ceiling). |
| Scheduled job | Azure 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 store | Azure Container Registry. Images are tagged with the short git commit SHA. |
| Database | PostgreSQL, single schema ai-yield, reached through DATABASE_URL. |
| Cache | Redis, reached through REDIS_URL over TLS. |
| Gateway | LiteLLM in a separate container; not deployed from this repository. |
| Identity | Microsoft 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.
| Variable | Purpose | Held as |
|---|---|---|
DATABASE_URL | PostgreSQL connection. | Secret |
REDIS_URL | Redis connection; needed for gateway replay protection and the live stream. | Secret |
AUTH_SECRET | Auth.js session signing. | Secret |
ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET, ENTRA_TENANT_ID | Entra ID app registration for staff sign-in. | Secrets |
AUTH_URL, AUTH_TRUST_HOST | Public base URL and proxy trust for Auth.js. | Plain |
CRON_SECRET | Authenticates the scheduled job to the job routes (x-cron-secret). | Secret on the app and on the job |
CREDENTIAL_ENCRYPTION_KEY | Encrypts provider credentials entered in the app. | Secret |
LITELLM_CALLBACK_SECRET | Verifies gateway callbacks; must match the gateway's value. | Secret |
AZURE_MI_CLIENT_ID | Selects the user-assigned managed identity used to read Azure billing. | Plain |
AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET | Service-principal fallback for Azure connections outside Azure. | As needed |
AZURE_ARM_TOKEN | Short-lived token for local Azure testing only. | Local .env only |
JOBS_BASE_URL, JOBS_LOOKBACK_HOURS | Where the job runner calls, and the re-pull window (default 72 hours). | Plain, on the job |
DEPLOYED_AT | Set to a timestamp on every deploy so a new revision always rolls. | Plain |
AUTH_BYPASS | Local 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.
| Target | What it does |
|---|---|
make dev | Start the local dev server. |
make test | Typecheck plus the unit tests. |
make db-push / db-studio | Apply the schema directly, or open Drizzle Studio. |
make seed-admin | Create a platform_admin user (EMAIL=, NAME=). |
make seed-demo | Seed the three demo tenants (ONLY=, CONFIRM_PROD=1). |
make build | Build the image for linux/amd64, tagged with the commit SHA. |
make push | Log in to the registry, build, push and deploy. |
make deploy | Sync the app's secrets and environment, then update the Container App image. TAG= deploys a specific image. |
make release | make test, then make push. |
make cron-deploy / cron-run / cron-logs | Create or update the scheduled job (idempotent), run it once now, list recent executions. |
make logs | Stream live Container App logs. |
make jobs-local | Run the same job runner against a local dev server. |
make arm-token / aws-creds | Refresh 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/.
| Workflow | Trigger | What it does |
|---|---|---|
ci.yml | Every pull request; reused by the deploy workflow | Typecheck, 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.yml | Every commit to main; manual dispatch | Runs 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
productionenvironment, 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 deployfrom a machine holding.env. - Gating. Add required reviewers to the
productionenvironment 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.
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
RoadmapThe 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-secretheader). - 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 platform | Model A (available now) |
| has strict data-residency or isolation requirements | Model B is the design answer, but it is not built. Raise with the product team, Legal and the Data Privacy Officer. |