Environments & Infrastructure
Three environments — Dev, Test, Prod (plus local). Each maps to a branch in both repos. Verified against commit history on 2026-06-24.
| Environment | Frontend — branch → host | Backend — branch → API |
|---|---|---|
| Dev | dev → dev.consumr.ai |
dev → dev-api.consumr.ai |
| Test | test → test.consumr.ai |
test → test-api.consumr.ai |
| Prod | main → app.consumr.ai |
app → app-api.consumr.ai (api.consumr.ai = legacy) |
| Local | feature branch → localhost | dev-api / 127.0.0.1:5001 |
📎 Evidence
- Dev FE branch is
dev—pw-enterprise-frontendgit —git branch -ashowsremotes/origin/dev; aDevworkflow exists in.github/workflows/dev.yml. ✅ - Dev FE host
dev.consumr.ai—pw-enterprise-frontend/src/api/config.js:144-152— hostname branch setsurl = dev,environment = 'dev'. ✅ - Dev BE branch is
dev—pw-enterprisegit —remotes/origin/devexists; backend.github/workflows/dev.ymlnameddev. ✅ - Dev BE API
dev-api.consumr.ai—pw-enterprise-frontend/src/api/config.js:18—const dev = 'https://dev-api.consumr.ai'assigned when hostname isdev.consumr.ai. ✅ - Test FE branch is
test—pw-enterprise-frontendgit —git branch -ashowsremotes/origin/testexists. (Finder wrongly marked this unverifiable; the branch IS present.) ✅ - Test FE host
test.consumr.ai—pw-enterprise-frontend/src/api/config.js:135-143— hostname branch setsurl = test,environment = 'test'. ✅ - Test BE branch is
test—pw-enterprisegit —remotes/origin/testexists. Note.github/workflows/test.ymlis an API-test workflow targetingalpha, NOT a test-env deploy. ✅ - Test BE API
test-api.consumr.ai—pw-enterprise-frontend/src/api/config.js:19,136—const test = 'https://test-api.consumr.ai'assigned when hostname istest.consumr.ai. ✅ - Prod FE branch is
main—pw-enterprise-frontendgit —git symbolic-ref refs/remotes/origin/HEAD=refs/remotes/origin/main;Mainworkflow runsmain-action.sh. ✅ - Prod FE host
app.consumr.ai—pw-enterprise-frontend/src/api/config.js:127-134— hostname branch setsurl = app,environment = 'app'. ✅ - Prod BE branch is
app—pw-enterprisegit —git symbolic-ref refs/remotes/origin/HEAD=refs/remotes/origin/app; local* app. ✅ - Prod BE API
app-api.consumr.ai—pw-enterprise-frontend/src/api/config.js:17,129—const app = 'https://app-api.consumr.ai'assigned underapp.consumr.ai. ✅ api.consumr.aiis legacy (not active) —pw-enterprise-frontend/src/api/config.js:15declaresconst api = 'https://api.consumr.ai'but no hostname branch assigns it tourl; active prod isapp-api.consumr.ai. Table now marks it legacy. ✅- Local FE = feature branch → localhost —
pw-enterprise-frontend/src/api/config.js:103-125— code routeslocalhost/local.consumr.aihostnames; the ‘feature branch’ part is convention, not code-enforced. ⚠️ - Local BE = dev-api / 127.0.0.1:5001 —
pw-enterprise-frontend/src/api/config.js:104,118—local.consumr.aidefaults to127.0.0.1:5001(viaVITE_API_URL); plainlocalhostsetsurl = dev. ✅
- Promotion flow:
dev → test → prod(visible in git history —testis fed by merges fromdev). - Environment selection: the frontend resolves its environment at runtime by hostname (
src/api/config.js), not a build flag. - Deploys: manual — GitHub Actions
workflow_dispatchon a self-hosted runner running shell scripts (dev-action.sh/main-action.shfor FE;pw-enterprise-dev-server.sh/pw-enterprise-main-server.shfor BE). The frontend dev deploy also purges the Cloudflare cache (the prodmain.ymlhas no purge job).
📎 Evidence
- Promotion
dev → test → prod—pw-enterprisegit merge log ontest— shows merges ofappINTOtestand PRs#9841 (I5216)etc.; a literalmerge dev into testwas not seen. Flow is convention-supported, not a hard pipeline. ⚠️ - Env resolved at runtime by hostname —
pw-enterprise-frontend/src/api/config.js:4,103-160— readswindow.location.hostnamethen if/else-if chain sets url/env/langgraph; onlyVITE_API_URLoverrides local URL. ✅ - Manual deploys via
workflow_dispatch—pw-enterprise/.github/workflows/dev.yml:2,Main.yml:3-4,pw-enterprise-frontend/.github/workflows/dev.yml:2,main.yml:2— all useon: workflow_dispatch. ✅ - Self-hosted runner —
pw-enterprise/.github/workflows/dev.yml:9&Main.yml:8,pw-enterprise-frontend/.github/workflows/dev.yml:9&main.yml:9— all jobs declareruns-on: self-hosted. ✅ - FE dev deploy runs
dev-action.sh—pw-enterprise-frontend/.github/workflows/dev.yml:11-15—cd /home/coder/rushab/; time ./dev-action.sh. Script lives on the runner, not in repo. ✅ - FE prod deploy runs
main-action.sh—pw-enterprise-frontend/.github/workflows/main.yml:11-15—cd /home/coder/rushab/; time ./main-action.sh. ✅ - BE dev deploy runs
pw-enterprise-dev-server.sh—pw-enterprise/.github/workflows/dev.yml:11-15—cd /home/coder/rushab/; time ./pw-enterprise-dev-server.sh. ✅ - BE prod deploy runs
pw-enterprise-main-server.sh—pw-enterprise/.github/workflows/Main.yml:10-14—cd /home/coder/rushab/; time ./pw-enterprise-main-server.sh. ✅ - FE cache purge is dev-only —
pw-enterprise-frontend/.github/workflows/dev.yml:18-26runspurge_profitwheel_cache.shafter deploy;main.yml(prod) has no purge job. Doc now scopes this to dev. ✅
📎 Evidence
- Stale
productionbranch exists (BE) —pw-enterprisegit —git branch -alistsremotes/origin/production. ✅ productionlast commit April 2024 —pw-enterprisegit —git log -1 remotes/origin/production=2024-04-11 17:51:49‘Added tracing for answer engine’. ✅- Real prod branch is
app(default) —pw-enterprisegit —git symbolic-ref refs/remotes/origin/HEAD=refs/remotes/origin/app; local checkout shows* app. ✅
Host topology
Section titled “Host topology”Verified against the VMs on 2026-06-25 (GCP Cloud Console SSH, read-only).
Two GCP Compute Engine VMs run all three environments. There is no separate test VM — test and production are co-located on pw-prod as two checkouts.
| VM | Cloud | Environments | Backend checkout(s) |
|---|---|---|---|
pw-dev |
GCP — project still-primer-308606, zone us-east1-c |
Dev | /home/coder/served/pw-enterprise (live) |
pw-prod |
GCP Compute Engine | Test and Production | test → /opt/test/pw-enterprise, prod → /opt/production/pw-enterprise |
Both run Ubuntu 22.04.5 LTS on GCP kernels (6.8.0-1044-gcp). A separate external API service runs on pw-prod at /home/coder/rushab/pw-external-api. Ingress on both is Apache 2.4.52 fronting the apps, with public exposure via a Cloudflare Tunnel (cloudflared) — not a public load balancer. The React frontend is hosted separately on Cloudflare Pages, not on these VMs.
📎 Evidence
- Two VMs, GCP — both prompts (
navdeep_singh@pw-dev,navdeep_singh@pw-prod);uname -r=6.8.0-1044-gcpon both;grep PRETTY_NAME /etc/os-release=Ubuntu 22.04.5 LTS. ✅ - pw-dev project/zone — GCP Cloud Console SSH URL:
.../projects/still-primer-308606/zones/us-east1-c/instances/pw-dev. ✅ (pw-prod’s project/zone not captured; GCP confirmed via-gcpkernel +google-cloud-ops-agentapt source.) - Test + prod co-located on pw-prod —
sudo find /var/www /home /opt /srv -maxdepth 5 -name uv.lockon pw-prod →/opt/test/pw-enterprise/uv.lock,/opt/production/pw-enterprise/uv.lock,/home/coder/rushab/pw-external-api/uv.lock. No separatepw-testhost was accessed. ✅ - Live dev checkout =
/home/coder/served/pw-enterprise— confirmed 2026-06-25; the/home/coder/pw-enterprisedirectory we first sampled is a separate, abandoned checkout (old pre-1.x deps) and is not what the dev service serves. 🔍 direct confirm:sudo readlink /proc/<dev-gunicorn-pid>/cwd. ✅ - Apache 2.4.52 ingress —
apache2 -von both =Server version: Apache/2.4.52 (Ubuntu);sudo ss -tlnpshowsapache2LISTEN on:80and:443. nginx is not installed (nginxcommand not found). ✅ - Public exposure via Cloudflare Tunnel —
sudo ss -tlnpshowscloudflaredlistening locally (pw-dev:46487, pw-prod:20241);/etc/apt/sources.list.d/cloudflared.listpresent. ✅ - Frontend on Cloudflare Pages (not the VMs) —
pw-enterprise-frontend/wrangler.toml:3(pages_build_output_dir); no frontend build is served from Apache on these boxes. ✅
Cloud Inventory
Section titled “Cloud Inventory”Inventory completed 2026-06-25 from VM access (previously “pending access”).
GCP (primary): Compute Engine (the pw-dev / pw-prod VMs), Cloud SQL (managed Postgres reached via cloud-sql-proxy on pw-dev), Secret Manager, BigQuery, and the Cloud Ops / OpenTelemetry agent stack. Cloudflare: Pages (frontend), Workers (edge bindings AI, Vectorize ×3, Hyperdrive), Cloudflare Tunnel (VM ingress), and R2 object storage (server-side via boto3, not a Worker binding). Azure: legacy — the app has migrated off (Key Vault → GCP Secret Manager, Blob → R2), but Azure monitoring agents are still running on both VMs (amacoreagent, change-tracking).
Runtime services & datastores (per VM)
Section titled “Runtime services & datastores (per VM)”| Service / tech | pw-dev | pw-prod (test + prod) |
|---|---|---|
| Web server (ingress) | Apache 2.4.52 | Apache 2.4.52 |
| Public ingress | Cloudflare Tunnel | Cloudflare Tunnel |
| App servers | gunicorn (:8080), Flask (:2096, :5000) |
gunicorn ×2 clusters (:8445, :8450), FastAPI (:8000) |
| PostgreSQL | local ×2 (:5432 = 12.12, :5433) |
local ×2 (:5432 = 12.12, :5433) |
| Managed DB | GCP Cloud SQL via cloud-sql-proxy (:5434) |
— (no Cloud SQL proxy) |
| MySQL | local 8.0.46 (:3306) |
local 8.0 (:3306) |
| Redis | local 8.8.0 (:6129) |
local 6.0.16 (:6129, bound to VPC IP 10.0.0.4) |
| RabbitMQ | 3.9.27 (:5672 AMQP) |
— (no RabbitMQ) |
| Job queue UI | RQ Dashboard (:9181) |
RQ Dashboard (:9181) |
| Observability | full OpenTelemetry Collector (OTLP/Jaeger/Zipkin) + GCP Ops + Fluent Bit + Datadog/Sentry | GCP Ops (otelopscol) + Fluent Bit + Sentry |
| Legacy agents | Azure Monitor (amacoreagent), Defender (mdatp) |
Azure Monitor (amacoreagent), change-tracking |
Elasticsearch is remote — it’s a backend dependency but is not listening on either VM (:9200 absent in ss -tlnp), so it’s a managed/remote cluster.
📎 Evidence
- Running services —
sudo ss -tlnpon pw-dev and pw-prod (2026-06-25). pw-dev: apache2 80/443, cloudflared, mysqld 3306, postgres 5432/5433, cloud-sql-proxy 5434, redis-server 6129, RabbitMQ (beam.smp/epmd5672/4369/25672), gunicorn 8080, flask 2096 + python 5000, rq-dashboard 9181, full otelcol (4317/4318/9411/14268…), otelopscol, fluent-bit, amacoreagent. pw-prod: apache2 80/443, cloudflared, fastapi 8000, gunicorn 8445 + 8450, mysqld 3306, postgres 5432/5433, redis-server on 10.0.0.4:6129, rq-dashboard 9181, containerd, otelopscol, fluent-bit, amacoreagent. ✅ - Postgres server 12.12 —
sudo -u postgres psql -tc 'select version();'on both =PostgreSQL 12.12 (Ubuntu 12.12-0ubuntu0.20.04.1). ✅ - Redis server versions differ across VMs —
redis-server --version: pw-dev =Redis server v=8.8.0, pw-prod =Redis server v=6.0.16. (Dev runs Redis 8.x; prod runs Redis 6.x.) ✅ - RabbitMQ dev-only —
sudo rabbitmqctl versionon pw-dev =3.9.27; pw-prodss -tlnpshows no AMQP/beam.smplistener. Likely the Airflow/Celery broker (Airflow runs on pw-dev:~coder/airflow/,airflows_monitor_dev.log); no app call-site verified. ⚠️ - Cloud SQL is dev-only in what we saw —
cloud-sql-proxyLISTEN on pw-dev:5434; absent on pw-prod. ✅ - MySQL 8.0.46 —
mysql --versionon both =8.0.46-0ubuntu0.22.04.3. ✅ - Elasticsearch remote —
:9200absent fromss -tlnpon both VMs; client dep present inuv.lock(see Tech Stack). ✅ - Azure agents still running (legacy) —
amacoreagent(Azure Monitor Agent) +change_trackingLISTEN on both VMs; matches the app’s Azure→GCP/Cloudflare migration documented inlibrary/storage.py/library/secrets.py. ✅
Deployment topology
Section titled “Deployment topology”
flowchart TB
U["Users / browser"] --> CFE["Cloudflare edge"]
CFE --> PAGES["Cloudflare Pages<br/>React 19 SPA (frontend)"]
CFE --> TUN["Cloudflare Tunnel<br/>(VM ingress — no public LB)"]
subgraph DEV["VM: pw-dev · GCP us-east1-c · DEV"]
AP1["Apache 2.4.52"] --> GU1["gunicorn :8080 + Flask"]
GU1 --> PG1[("PostgreSQL 12.12 ×2")]
GU1 --> MY1[("MySQL 8.0.46")]
GU1 --> RD1[("Redis 8.8.0")]
AF1["Airflow + RabbitMQ 3.9.27<br/>(dev only)"]
CS1["cloud-sql-proxy → GCP Cloud SQL<br/>(dev only)"]
GU1 -.-> CS1
end
subgraph PROD["VM: pw-prod · GCP · TEST + PROD co-located"]
AP2["Apache 2.4.52"] --> GU2["gunicorn :8445 + :8450"]
AP2 --> FA["FastAPI :8000"]
GU2 --> TST["/opt/test<br/>langchain 1.3.11"]
GU2 --> PRD["/opt/production<br/>langchain 1.2.7"]
GU2 --> PG2[("PostgreSQL 12.12 ×2")]
GU2 --> MY2[("MySQL 8.0")]
GU2 --> RD2[("Redis 6.0.16")]
end
TUN --> AP1
TUN --> AP2
ES[("Elasticsearch<br/>remote / managed")]
GU1 -.-> ES
GU2 -.-> ES
Two GCP VMs, three environments. The frontend is served from Cloudflare Pages; backend ingress is the Cloudflare Tunnel → Apache → gunicorn/FastAPI. RabbitMQ and the Cloud SQL proxy are dev-only; Elasticsearch is remote. (Evidence: same ss -tlnp / find uv.lock / version captures cited above.)
📎 Evidence
- GCP Secret Manager (primary) —
pw-enterprise/library/gcp_secrets.py:1-18&pyproject.toml:101— importsgoogle.cloud.secretmanager; SDKgoogle-cloud-secret-manager>=2.26.0declared (line 101, not 100). ✅ - GCP BigQuery —
pw-enterprise/api/common.py:4,11&pyproject.toml:100—from google.cloud import bigquery;client = bigquery.Client(); SDKgoogle-cloud-bigquery>=3.39.0declared. ✅ - Azure Key Vault is legacy —
pw-enterprise/library/gcp_secrets.py:3-5(docstring ‘replacing Azure Key Vault’),library/secrets.py:109-116(*_key_vaultfns delegate to GCP),pyproject.toml:27-28,.env:91-93(AZURE_*keys empty). ✅ - Cloudflare Pages (FE hosting) —
pw-enterprise-frontend/wrangler.toml:3—pages_build_output_dir = "dist"is the Cloudflare Pages config key. ✅ - Cloudflare Workers —
pw-enterprise-frontend/src/api/config.js:13(a*.profitwheel.workers.devworker URL) &pw-enterprise/pyproject.toml:31(cloudflare>=4.3.1SDK). ✅ - Workers AI binding —
pw-enterprise-frontend/wrangler.toml:13-14,22-23—[ai] binding = "AI"for both default and[env.production]. ✅ - Vectorize binding (×3 indices) —
pw-enterprise-frontend/functions/_middleware.ts:11-13andworker-configuration.d.ts:9-11declareVECTORIZE_INDEX_SMALL/BASE/LARGE: VectorizeIndexin the WorkerEnv; configured via the Pages dashboard (onlyAIis declared inwrangler.toml), which is whywrangler.tomlalone doesn’t list it. ✅ - Hyperdrive binding —
pw-enterprise-frontend/functions/_middleware.ts:14(PRODDB: Hyperdrive) andworker-configuration.d.ts:12(DEVDB: Hyperdrive) declare it in the WorkerEnv; configured via the Pages dashboard, notwrangler.toml. ✅ - Cloudflare R2 (object storage, server-side) —
pw-enterprise/library/storage.py:1-7,32-37builds a boto3 S3 client against*.r2.cloudflarestorage.com;.env:144-148define the R2 keys. Used server-side, not as a Worker edge binding. ✅
📎 Evidence
- Inventory completed from VM access —
sudo ss -tlnp,sudo find … -name uv.lock,apache2 -v,redis-server --version,sudo rabbitmqctl version,sudo -u postgres psql -tc 'select version()', and the per-checkoutuv.lockextracts, all run on pw-dev and pw-prod on 2026-06-25. Raw capture:docs/vm-tech-inventory.md. ✅ - Open items — deploy-script contents, dev serving-path confirmation, and edge/network config were explicitly not gathered in this pass. 🔍 needs: one more read-only session (read the scripts;
sudo ss -tnp | grep <gunicorn pid>working dir via/proc/<pid>/cwd). ✅