Three of these four environments run the same code today: the local product stack and the AWS/Azure Terraform and Bicep templates all build from this repo’s current
Dockerfile (target: services.api.product_app:app, the allowlisted ~44-operation surface) and ../frontend/Dockerfile. The live Render+Vercel demo (api.causeloop.ai / app.causeloop.ai) is pinned to the release_demo branch, which predates the product_app.py split — see Render + Vercel demo for exactly what that means for what’s actually deployed there today.The four environments
The AWS and Azure templates target the same
product_app.py/docker-compose.product.yml architecture as the local stack. As of this writing that architecture has not yet been cut over to the live demo — see the note above. docs/CLOUD_DEPLOYMENT.md’s release order (build images → apply infra with services off → migrate → bootstrap → activate) applies to AWS and Azure; the Render demo has its own, simpler flow documented on Render + Vercel demo.
Concern matrix
Concern-by-concern detail
API host and network exposure
API host and network exposure
The API is private in every environment except the fact that it must be reachable from its own frontend. Locally,
api is only published to 127.0.0.1:18000 — reachable from your machine, not your local network. On AWS, the frontend resolves the API through Cloud Map service discovery (http://api.<namespace>:8000) rather than an internal load balancer; there is no public ALB listener for the API at all. On Azure, the API Container App has no external ingress; the frontend reaches it over the Container Apps environment’s internal DNS. The Render demo is the one environment where the API has its own public hostname (api.causeloop.ai) — a deliberate simplification for the demo, not the pattern the other three environments follow.Postgres
Postgres
Every environment runs Postgres 16. Locally that’s the
pgvector/pgvector:pg16 image (pgvector support carried over from the legacy MVP1 embedding-store schema); AWS and Azure use their respective managed Postgres 16 offerings; Render’s managed Postgres also targets major version 16 per the Blueprint’s postgresMajorVersion: "16". Only local and the managed clouds distinguish an owner/migrator role (MIGRATIONS_DATABASE_URL) from the runtime app_rw role — see Migrations for how that boundary is enforced.Object store
Object store
MinIO (local) is API-compatible with S3, so the exact same object-store client code that talks to MinIO locally talks to real S3 on AWS with no code change — only
CAUSELOOP_OBJECT_PROVIDER and endpoint configuration differ. Azure’s Blob Storage is a distinct provider path in the same object-store abstraction. The current Render Blueprint configures none of this — see the caution below.Redis
Redis
Redis backs only the login rate limiter in every environment — it is never a business-data or job authority (losing it resets rate-limit windows, never datasets, uploads, or jobs, per
docs/CLOUD_OPERATIONS.md). Locally it’s a plain redis:7.4-alpine container with append-only-file persistence; AWS uses ElastiCache with TLS and an AUTH token; Azure deliberately uses Azure Managed Redis rather than the classic Azure Cache for Redis, which is on a retirement path — the Bicep template configures it for the classic client protocol so the standard redis-py client works unmodified.Email
Every environment that implements the newer product architecture (local, AWS, Azure) uses the same pattern: the API atomically commits an encrypted
send_email row to control.platform_jobs in the same transaction as the auth-state change it’s notifying about, and only the product worker ever decrypts and sends it, with bounded retry. Only the provider underneath that pattern changes — Mailpit locally, Amazon SES on AWS, Azure Communication Services Email on Azure. The current Render Blueprint predates this pattern entirely (see below).Workers
Workers
Local, AWS, and Azure all run exactly two workers with the job-type split described below. The current Render Blueprint runs one undifferentiated worker (
causeloop-pipeline-worker) — there is no provisioner/product privilege split on the live demo today.Migrations
Migrations
Local, AWS, and Azure all invoke the identical
python scripts/migrate_product.py as a one-off task before any long-running service starts. The Render Blueprint instead runs alembic upgrade inline in the web service’s own startCommand, ahead of uvicorn — a materially different (and, as documented on Render + Vercel demo, already stale) approach.What’s identical everywhere
- Container images. The local stack, AWS, and Azure all build (or pull) the same two images: this repo’s root
Dockerfile(non-root FastAPI API/worker/provisioner/migration image,CMDtargetsservices.api.product_app:app) and../frontend/Dockerfile(non-root Next.js standalone server). AWS and Azure reference these as immutable, pre-pushed image URIs rather than building them —terraform applyandaz deploymentdo not build containers. - The readiness gate.
GET /health/ready(implemented inservices/api/product_app.py) checks database connectivity, bothproduct-workerandprovisioner-workerheartbeats (with their required job-type sets), object-storage reachability, credential-encryption configuration, email-provider configuration, and Redis — in every environment that runs this app.docs/CLOUD_OPERATIONS.mdis explicit that readiness, not liveness, is the deployment and rollback gate:/health/liveonly proves the process can serve, and traffic should never be routed on liveness alone. - The privilege boundary. The normal API/product-worker runtime identity only ever receives an application-role database DSN (
APP_DATABASE_URL, theapp_rwPostgres role) and, where applicable, Redis credentials. Only the one-off migration task and the provisioner worker receive owner/migrator credentials (MIGRATIONS_DATABASE_URL) capable of schema DDL. This boundary is enforced identically in the Compose file’s environment blocks, the AWS Secrets Manager IAM scoping, and the Azure Key Vault RBAC scoping. - The release order for a fresh environment. Provision infrastructure with long-running services disabled → run the one-off migration → run the idempotent administrator-bootstrap task → enable services → verify
/health/ready→ exercise one real invitation, one upload, and tenant isolation before admitting production data. AWS and Azure both implement this order explicitly (activate_services/activateServicesgates); the local stack’sdocker compose up -d --buildachieves the same order via the Composedepends_on: condition:graph. - Object-store write semantics. Wherever objects are written — MinIO locally, S3 on AWS, Blob on Azure — writes are conditional at the provider API (
If-None-Match: *on S3, an if-missing condition on Azure blocks), not merely collision-resistant by naming convention, so a duplicate key can never silently replace bytes whose hash is already recorded in PostgreSQL.
What varies
- Who provisions the data plane. Locally you own every container; on Render, Render owns the managed Postgres/Redis instances; on AWS/Azure, Terraform/Bicep own them, and the account owner is responsible for VPC/VNet, DNS, TLS certificates, and IAM/RBAC boundaries around them.
- How migrations run. The local
migratecontainer and the AWS/Azure one-off tasks all invoke the samescripts/migrate_product.py; the live Render demo instead runsalembic upgradedirectly in the web service’sstartCommand, ahead ofuvicornstarting — see Migrations for the full picture, including why that inline approach is already fragile. - Email and object-store providers. These are the two dependencies the local stack replaces with pure emulators (Mailpit, MinIO) that have no cloud equivalent deployed anywhere — they exist only to make local development observable, never as a promoted component.
- Branch and deploy trigger. The Render/Vercel demo deploys from
release_demoon agit push; AWS/Azure deploys areterraform apply/Bicep deployments run by an operator against whatever image tag was just built, independent of any specific branch push. - Scaling and plan tier. The Render Blueprint pins every service (
causeloop-postgres,causeloop-redis,causeloop-api,causeloop-pipeline-worker) to thestarterplan — sized for a demo, not for production load. AWS and Azure expose this as explicit input variables instead of a fixed plan name: AWS’sdb_instance_class(defaultdb.t4g.micro),backend_cpu/backend_memory, andenvironment(which toggles RDS/ElastiCache Multi-AZ); Azure’s equivalent Bicep parameters. Neither cloud template hardcodes a “starter” tier the way the Blueprint does.
Worker job types by environment
CAUSELOOP_WORKER_COMPONENT and CAUSELOOP_WORKER_JOB_TYPES are what separate the two worker privilege levels in every environment that runs the product_app.py architecture (local, AWS, Azure) — the API’s own /health/ready check verifies each required component has advertised exactly its expected job types via a recent control.runtime_heartbeats row:
This is the same distinction described in Local product stack and Workers and jobs; it’s restated here because it’s one of the few things that is identical in shape (two workers, two privilege levels) across local/AWS/Azure but does not exist at all in the current Render Blueprint, which runs a single
causeloop-pipeline-worker with no such split — see Render + Vercel demo.
Choosing an environment for a given task
Reproducing or debugging a production-shaped issue
Reproducing or debugging a production-shaped issue
Use the local product stack. It’s the only environment where you can attach a debugger, tail every container’s logs at once, and rehearse a backup/restore cycle (
scripts/rehearse_product_backup.sh) without touching a shared resource. See Local parity stack.Shipping a fix the live demo needs today
Shipping a fix the live demo needs today
Land the fix on
main, then merge or cherry-pick it into release_demo and push. See Render + Vercel demo for the full branch-strategy walkthrough and the Vercel commit-author gotcha.Standing up a real customer-facing production environment
Standing up a real customer-facing production environment
Use
deploy/aws or deploy/azure — pick one provider, don’t split across both. Both are validated IaC that provisions the full production-parity contract (private data plane, envelope-encrypted credentials, SES/ACS email, alarms/Log Analytics), but account-specific apply/DNS/certificate/quota work remains an operator gate before real tenant data should land there. See AWS deployment.