> ## Documentation Index
> Fetch the complete documentation index at: https://docs.causeloop.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Cloud environments

> The environment matrix across local Compose, the Render+Vercel demo, AWS Terraform, and Azure Bicep — what is identical everywhere and what varies.

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.

<Note>
  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](/deployment/render-vercel-demo) for exactly what that means for what's actually deployed there today.
</Note>

## The four environments

| Environment          | Defined by                                                                          | Status                                                                                                                          |
| -------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Local product stack  | `infra/docker-compose.product.yml`                                                  | The canonical deployment-parity dev environment; every engineer runs this                                                       |
| Render + Vercel demo | Render Blueprint (`render.yaml`, `release_demo` branch) + Vercel project `frontend` | Live today at `app.causeloop.ai` / `api.causeloop.ai`, "as of July 2026"                                                        |
| AWS                  | `deploy/aws` (Terraform)                                                            | Validated IaC, locally proven; account-specific `apply`/DNS/quota work is an operator gate before it's a live production target |
| Azure                | `deploy/azure` (Bicep)                                                              | Validated IaC, locally proven; same operator gate as AWS                                                                        |

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](/deployment/render-vercel-demo).

## Concern matrix

| Concern               | Local Compose                                                                  | Render + Vercel demo                                                                                                                                         | AWS                                                                                                                                      | Azure                                                                                                                |
| --------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Frontend host         | `frontend` container, `127.0.0.1:13000`                                        | Vercel project `frontend`, `app.causeloop.ai`                                                                                                                | ECS/Fargate behind an internet-facing ALB, `public_hostname` via Route 53                                                                | Container Apps (external), custom domain                                                                             |
| API host              | `api` container, `127.0.0.1:18000`, private to the Compose network             | Render web service `causeloop-api`, `api.causeloop.ai` (CNAME to `causeloop-backend.onrender.com` — see caution below)                                       | Private ECS/Fargate service, resolved by the frontend over AWS Cloud Map service discovery (`http://api.<namespace>:8000`), never public | Container Apps (internal-only), resolved over the Container Apps environment's internal DNS                          |
| Postgres              | `pgvector/pgvector:pg16` container, named volume `product_postgres`            | Render managed Postgres (`causeloop-postgres`, plan `starter`, major version 16)                                                                             | `aws_db_instance.main` — RDS PostgreSQL 16, private subnets, Multi-AZ in `production`                                                    | `Microsoft.DBforPostgreSQL/flexibleServers`, private, VNet-integrated                                                |
| Object store          | MinIO (`minio/minio`), S3-API-compatible, versioned bucket `causeloop-uploads` | Not configured by the Blueprint — the `release_demo`-era app predates the S3/Blob object-store split                                                         | Private, versioned, KMS-encrypted `aws_s3_bucket.objects`                                                                                | Private, versioned Blob Storage container behind a private endpoint                                                  |
| Redis                 | `redis:7.4-alpine`, append-only file, named volume `product_redis`             | Render managed Redis (`causeloop-redis`, plan `starter`)                                                                                                     | `aws_elasticache_replication_group.main`, TLS + AUTH token                                                                               | Azure Managed Redis (`redisEnterprise`, classic protocol) — not Azure Cache for Redis, which is on a retirement path |
| Email                 | Mailpit (loopback SMTP capture, `127.0.0.1:18025` UI)                          | Not configured by the Blueprint                                                                                                                              | Amazon SES, one verified sender identity, production access required before real invitations send                                        | Azure Communication Services Email, preverified Azure-managed sender domain                                          |
| Workers               | `product-worker` + `provisioner-worker` containers, `restart: unless-stopped`  | One `causeloop-pipeline-worker` Render background worker (`release_demo` branch)                                                                             | Two ECS services (`product_worker`, `provisioner_worker`), `desired_count` gated by `activate_services`                                  | Two Container Apps (product and provisioner privilege levels)                                                        |
| Migrations            | One-off `migrate` container (`scripts/migrate_product.py`), `restart: "no"`    | `startCommand` runs `alembic upgrade` inline before `uvicorn` starts (see caution below)                                                                     | One-off ECS task, run via `deploy/aws/run-migration.sh`                                                                                  | One-off Container Apps Job, started with `az containerapp job start`                                                 |
| Credential encryption | Local Fernet key (`SOURCE_CREDS_KEY`, `CAUSELOOP_SECRET_PROVIDER: fernet`)     | Fernet key via a manually-set `SOURCE_CREDS_KEY` secret (`sync: false`) on both `causeloop-api` and `causeloop-pipeline-worker` — no KMS envelope encryption | Envelope encryption with a customer-managed KMS key (`CAUSELOOP_SECRET_PROVIDER=aws-kms`)                                                | Envelope encryption with Key Vault                                                                                   |
| Logs/observability    | Container logs via `docker compose logs`                                       | Render's built-in service logs                                                                                                                               | CloudWatch Logs (per-component log groups) + Container Insights                                                                          | Log Analytics                                                                                                        |

## Concern-by-concern detail

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="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](/deployment/migrations) for how that boundary is enforced.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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).
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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](/deployment/render-vercel-demo), already stale) approach.
  </Accordion>
</AccordionGroup>

## 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](/deployment/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:

| Component            | Job types                                                  | Credentials                                                                                                                |
| -------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `product-worker`     | `materialize_insights`, `send_email`, `train_tenant_model` | `APP_DATABASE_URL` only — no schema-DDL access                                                                             |
| `provisioner-worker` | `provision_tenant`                                         | `APP_DATABASE_URL` **and** `MIGRATIONS_DATABASE_URL` (owner DSN, to run `CREATE SCHEMA` and per-tenant Alembic migrations) |

This is the same distinction described in [Local product stack](/onboarding/local-product-stack) and [Workers and jobs](/architecture/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](/deployment/render-vercel-demo).

## Choosing an environment for a given task

<AccordionGroup>
  <Accordion title="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](/deployment/local-parity-stack).
  </Accordion>

  <Accordion title="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](/deployment/render-vercel-demo) for the full branch-strategy walkthrough and the Vercel commit-author gotcha.
  </Accordion>

  <Accordion title="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](/deployment/aws).
  </Accordion>
</AccordionGroup>

<Warning>
  `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](/deployment/render-vercel-demo) for the full discrepancy.
</Warning>

## Related

* [Local product stack](/onboarding/local-product-stack)
* [Local parity stack](/deployment/local-parity-stack)
* [AWS deployment](/deployment/aws)
* [Migrations](/deployment/migrations)
