Skip to main content
Causeloop’s product surface (frontend + FastAPI API + two workers) runs in four topologies that share the same two container images and the same readiness contract, but differ in how each stateful dependency is provisioned. Reading this page top to bottom tells you which environment a given fact belongs to before you go looking for it in the deeper pages.
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

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.
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.
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 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.
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).
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.
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, CMD targets services.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 apply and az deployment do not build containers.
  • The readiness gate. GET /health/ready (implemented in services/api/product_app.py) checks database connectivity, both product-worker and provisioner-worker heartbeats (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.md is explicit that readiness, not liveness, is the deployment and rollback gate: /health/live only 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, the app_rw Postgres 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 / activateServices gates); the local stack’s docker compose up -d --build achieves the same order via the Compose depends_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 migrate container and the AWS/Azure one-off tasks all invoke the same scripts/migrate_product.py; the live Render demo instead runs alembic upgrade directly in the web service’s startCommand, ahead of uvicorn starting — 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_demo on a git push; AWS/Azure deploys are terraform 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 the starter plan — sized for a demo, not for production load. AWS and Azure expose this as explicit input variables instead of a fixed plan name: AWS’s db_instance_class (default db.t4g.micro), backend_cpu/backend_memory, and environment (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

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.
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.
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.
render.yaml names the Render web service causeloop-api, but the live hostname resolves to causeloop-backend.onrender.com. That mismatch means the live Render web service was most likely created manually (or renamed) rather than being purely Blueprint-managed — do not assume every setting on the live service matches render.yaml exactly. See Render + Vercel demo for the full discrepancy.