Two lookup tables — one for production, one for development — covering the public hostnames, the backend API base URL, the Auth0 tenant, the operator tools, the data stores and the environment-specific settings. Both tables use the same row keys in the same order, so you can read one row across the two to see exactly what differs. Every row names where the value is actually defined, so you can change it at the source rather than guessing.
The third column is the point of the page. A value that is wrong in one place is usually wrong because it was changed in the app but not in the infrastructure, or vice versa.
Runtime and build values live as GitHub Actions variables and secrets on the dev and prod Environments in the ambasdr repo. The deploy workflows read them and render /opt/ambasdr/.env.backend on the Droplet. Inspect with gh variable list --env prod.
Infrastructure shape — Droplet size, DNS records, Cloudflare Access applications, whether Postgres and object storage are managed or in-box — lives in the separate ambasdr-infra repo under environments/<env>.tfvars. Both environments share one set of Terraform modules and differ only by these toggles.
A few values are literals inside .github/workflows/deploy-dev.yml and deploy-prod.yml rather than variables — the Spaces bucket names, the Stripe redirect URLs, and the admin-panel Cloudflare Access identifiers. Changing those means editing the workflow.
Passwords, API keys and signing secrets are stored as GitHub Actions secrets and are never printed here. Where a secret governs a row, the row names the secret (for example PROD_PG_HOST) instead of its value. Client IDs, Auth0 tenant domains, Stripe price IDs and Cloudflare Access audience tags are public identifiers, not secrets, so they are shown in full.
Deployed by the deploy-prod GitHub Actions workflow (manual workflow_dispatch only) onto the ambasdr-prod Droplet. Stateful data lives in DigitalOcean managed services rather than in the box.
| Concern | Value | Defined in |
|---|---|---|
| Primary domain (web app) | https://ambasdr.com — the bare apex, no subdomain |
ambasdr-infra/environments/prod.tfvars: domain = "ambasdr.com", subdomain = "" |
| Backend API base URL | https://api.ambasdr.com |
Cloudflare A record → the prod reserved IP; Caddy on the box reverse-proxies to backend:9080 |
| API URL baked into the web bundle | https://api.ambasdr.com |
GitHub prod Environment variable VITE_API_URL, passed as a Docker build arg. Vite inlines it at build time, so a dev image can never be promoted to prod. |
| CORS allowed origins | https://ambasdr.com |
GitHub prod variable PROD_ALLOWED_ORIGINS → ALLOWED_ORIGINS in /opt/ambasdr/.env.backend |
| Auth0 tenant | ambasdr-prod.us.auth0.com |
GitHub prod variable VITE_AUTH0_DOMAIN. The same value is handed to the backend as AUTH0_DOMAIN (deploy-prod.yml:208), so the SPA and the JWT validator always agree. |
| Auth0 SPA client ID | chegHzcSAPP6nASMx8UPRIGg7VynoUtH |
GitHub prod variable VITE_AUTH0_CLIENT_ID |
| Auth0 API audience | https://api.ambasdr.com — a logical identifier string, not a URL that is called |
GitHub prod variable VITE_AUTH0_AUDIENCE. Deliberately identical in both environments; the tenant is what separates them. |
| Admin control panel | https://panel.ambasdr.com/panel |
Published by the prod Cloudflare Tunnel (panel → backend:9080) behind the "Panel (prod)" Access application. App and policy IDs in prod.tfvars; the tunnel ingress and the proxied CNAME are managed through the Cloudflare API, outside Terraform. |
| Panel Access audience tag | 1097f2d195388abe4c160bdde7c24930608f73adadd5c43735e46b947b85e174 |
Literal CF_ACCESS_AUD in deploy-prod.yml. The backend checks the aud claim of the Cloudflare Access JWT against it. It is per-application, so it must differ from dev and changes if the Access app is recreated. |
| Grafana (logs + metrics) | https://grafana.ambasdr.com |
Cloudflare Tunnel + Access, email one-time PIN to any @juelz.ai address. App/policy IDs in prod.tfvars. |
| Portainer (container admin) | https://portainer.ambasdr.com |
Cloudflare Tunnel + Access, same PIN gate. App/policy IDs in prod.tfvars. |
| Droplet | ambasdr-prod — s-4vcpu-8gb, region nyc3, backups on |
prod.tfvars: droplet_size, enable_droplet_backups = true |
| Public IP | 209.38.50.179 (reserved IP) |
DigitalOcean reserved IP attached to the Droplet; the Cloudflare A records for the apex and api. point here. |
| Postgres | DigitalOcean Managed Postgres cluster, reached over the private VPC | prod.tfvars: managed_postgres_enabled = true. Connection details come from the GitHub secrets PROD_PG_HOST, PROD_PG_USER, PROD_PG_PASSWORD. |
| Object storage | Spaces bucket ambasdr-prod-documents, region nyc3, endpoint https://nyc3.digitaloceanspaces.com |
Literals in deploy-prod.yml (DO_SPACES_BUCKET / _REGION / _ENDPOINT); bucket created by spaces_enabled = true in prod.tfvars. |
| Public URL base for stored files | https://api.ambasdr.com/v1 |
GitHub prod variable PROD_DO_SPACES_PUBLIC_URL_BASE. Documents are served back through the API, not straight off the bucket. |
Backend ENVIRONMENT value |
production |
Literal in deploy-prod.yml. It selects production behaviour throughout the Go service and keeps the admin panel's development-only bypass path unreachable. |
| Stripe mode | Live mode | GitHub secrets PROD_STRIPE_SECRET_KEY and PROD_STRIPE_WEBHOOK_SECRET |
| Stripe webhook endpoint | https://api.ambasdr.com/webhooks/stripe |
Route registered at backend/internal/handler/router.go:415 (signature-verified, no JWT). The endpoint itself is registered in the Stripe dashboard / CLI, not in this repo. |
| Stripe redirect URLs | success https://ambasdr.com/dashboard?checkout=success&session_id={CHECKOUT_SESSION_ID}cancel https://ambasdr.com/dashboard?checkout=canceledbilling portal return https://ambasdr.com/dashboard |
Literals in deploy-prod.yml:198-200 |
| Stripe price IDs | Pro price_1TOtBvBrSVDg8X2uxCIOYUx0Premium price_1Tjfx2BrSVDg8X2uBitJXSUc |
GitHub prod variables PROD_STRIPE_PRICE_ID_PRO / _PREMIUM |
| Stripe payment link | https://buy.stripe.com/bJe5kCfE2026fWh3oOb3q01 |
GitHub prod variable VITE_STRIPE_PAYMENT_LINK. Set on prod only. |
| Terraform state | Spaces bucket ambasdr-tfstate, key prod/terraform.tfstate |
ambasdr-infra/environments/prod.s3.tfbackend |
| Deploy trigger | Manual only — workflow_dispatch on deploy-prod |
.github/workflows/deploy-prod.yml |
Deployed by the deploy-dev workflow onto the ambasdr-dev Droplet. Same rows, same order as the production table above. The defining difference: every data store runs as a container inside the box, so there are no managed-service endpoints.
| Concern | Value | Defined in |
|---|---|---|
| Primary domain (web app) | https://dev.ambasdr.com |
ambasdr-infra/environments/dev.tfvars: domain = "ambasdr.com", subdomain = "dev" |
| Backend API base URL | https://api.dev.ambasdr.com |
Cloudflare A record → the dev reserved IP; Caddy reverse-proxies to backend:9080 |
| API URL baked into the web bundle | https://api.dev.ambasdr.com |
GitHub dev Environment variable VITE_API_URL |
| CORS allowed origins | https://dev.ambasdr.com |
GitHub dev variable DEV_ALLOWED_ORIGINS → ALLOWED_ORIGINS in /opt/ambasdr/.env.backend |
| Auth0 tenant | dev-p63lq2om24unmpa2.us.auth0.com |
Repository-level variable VITE_AUTH0_DOMAIN. The dev Environment defines no override, so it inherits the repo value — unlike prod, which overrides it. A separate Auth0 tenant from prod, so dev logins and prod logins are entirely independent user pools. |
| Auth0 SPA client ID | SUxT7E56NlA9iarQEvryB8ZBANaAW3HT |
Repository-level variable VITE_AUTH0_CLIENT_ID (inherited, as above) |
| Auth0 API audience | https://api.ambasdr.com — same identifier string as prod, by design |
Repository-level variable VITE_AUTH0_AUDIENCE |
| Admin control panel | https://panel-dev.ambasdr.com/panel |
Dev Cloudflare Tunnel (panel-dev → backend:9080) behind the "Panel (dev)" Access application. App and policy IDs in dev.tfvars; tunnel ingress and CNAME managed via the Cloudflare API. |
| Panel Access audience tag | 6910108429833c4b211a1b633c2992683ce14c80929cbb9745f1e903dbd591f5 |
Literal CF_ACCESS_AUD in deploy-dev.yml. Different from prod's because it identifies a different Access application — never copy one environment's value to the other. |
| Grafana (logs + metrics) | https://grafana-dev.ambasdr.com |
Cloudflare Tunnel + Access. Dev additionally owns the account-global one-time-PIN login method (manage_grafana_idp = true in dev.tfvars; prod sets it false so the two do not fight over the same object). |
| Portainer (container admin) | https://portainer-dev.ambasdr.com |
Cloudflare Tunnel + Access. App/policy IDs in dev.tfvars. |
| Droplet | ambasdr-dev — s-2vcpu-4gb, region nyc3, backups off |
dev.tfvars: droplet_size, enable_droplet_backups = false |
| Public IP | 152.42.153.142 (reserved IP). The Droplet also carries its original public address 45.55.86.15; DNS points at the reserved IP. |
DigitalOcean reserved IP attached to the Droplet |
| Postgres | In-box postgres:16-alpine container, data under /data/ambasdr/postgres |
dev.tfvars: managed_postgres_enabled = false. The backend reaches it at DB_HOST=postgres over the Docker network. |
| Object storage | In-box MinIO container. Bucket ambasdr-documents, region us-east-1, endpoint http://minio:9000 |
Literals in deploy-dev.yml; spaces_enabled = false in dev.tfvars means no Spaces bucket is created. The region string is a MinIO placeholder, not a DigitalOcean region. |
| Public URL base for stored files | https://api.dev.ambasdr.com/v1 |
GitHub dev variable DEV_DO_SPACES_PUBLIC_URL_BASE |
Backend ENVIRONMENT value |
dev |
Literal in deploy-dev.yml. Note it is dev, not development — the admin panel's local bypass is gated on the exact string development, so on the dev box that path stays unreachable and Cloudflare Access is the only way in. |
| Stripe mode | Test mode | GitHub secrets DEV_STRIPE_SECRET_KEY and DEV_STRIPE_WEBHOOK_SECRET. Test and live are separate Stripe endpoints with separate signing secrets. |
| Stripe webhook endpoint | https://api.dev.ambasdr.com/webhooks/stripe |
Same route as prod (router.go:415); registered against the test-mode Stripe account. |
| Stripe redirect URLs | success https://dev.ambasdr.com/dashboard?checkout=success&session_id={CHECKOUT_SESSION_ID}cancel https://dev.ambasdr.com/dashboard?checkout=canceledbilling portal return https://dev.ambasdr.com/dashboard |
Literals in deploy-dev.yml:250-252 |
| Stripe price IDs | Pro price_1TyPvRBrSVDg8X2uut2JtYbMPremium price_1TyPvRBrSVDg8X2uCLri8xxI |
GitHub dev variables DEV_STRIPE_PRICE_ID_PRO / _PREMIUM |
| Stripe payment link | Not set on dev | No VITE_STRIPE_PAYMENT_LINK variable exists on the dev Environment. |
| Terraform state | Spaces bucket ambasdr-tfstate, key dev/terraform.tfstate |
ambasdr-infra/environments/dev.s3.tfbackend |
| Deploy trigger | deploy-dev workflow |
.github/workflows/deploy-dev.yml |
| Dev-only frontend flags | VITE_PRELAUNCH_MODE=false, plus a VITE_BETA_ACCESS_CODE_HASH gate |
GitHub dev Environment variables. Neither variable is defined on prod. |
These are deliberately not split per environment. Knowing which values are shared is as useful as knowing which differ — changing one of these affects dev and prod at the same time.
ambasdr.com is authoritative on Cloudflare (zone a8ca1d169250d44aa0c2972a679eedc2, account cec15730cfe2aaff791af22c8dca5269). Both environments are subdomains of the one zone. The zone itself is created once, by dev — prod.tfvars sets create_domain = false so the prod apply does not try to recreate it.ambasdr.cloudflareaccess.com, with the same "Allow juelz.ai" one-time-PIN policy shape on every gated app. Only the per-application audience tag differs.https://api.ambasdr.com in both. It is an identifier for the API registered in Auth0, not a hostname anything connects to, so it does not need to be per-environment. The tenant is what isolates the environments.wss://ambasdr-vomhblce.livekit.cloud, one project serving both, with separate API key/secret pairs per environment.https://us.cloud.langfuse.com for both. The environments are separated by their key pairs (DEV_LANGFUSE_* vs PROD_LANGFUSE_*), which point at different Langfuse projects.https://api.openai.com/v1, with a per-environment API key.127.0.0.1:9080, frontend 127.0.0.1:8080, knowledge-graph http://knowledge-graph:8050, Neo4j as an in-box container. Only Caddy listens on the public internet (ports 80 and 443).https://diorama.ambasdr.com, a single Cloudflare Pages project behind Cloudflare Access. It has no per-environment split; it publishes from main whenever diorama-docs/ changes.Because subdomain = "", the production React app is served from ambasdr.com itself rather than a subdomain such as app.ambasdr.com. That means the apex record cannot simultaneously point at a separate marketing host. If the marketing site ever needs the apex back, the app has to move to a subdomain first, and changing subdomain in prod.tfvars is only the first step. Four other places hardcode https://ambasdr.com and would each have to change too: PROD_ALLOWED_ORIGINS, the Auth0 callback / logout / web-origin URLs on the prod SPA application, the three Stripe redirect URLs in deploy-prod.yml, and the Cloudflare A record. The API hostname (api.ambasdr.com) is already a subdomain and would be unaffected.
How the boxes are provisioned, how the images are built and shipped, and the step-by-step first-deploy and rollback procedures behind the values on this page.