Development Environments & Workflow Runbook
Source:
Synthesized from the verified per-asset runbooks (nest, dev-nest, api, api-staging, rds-prod, rds-dev) and team workflow policy. Branch/env enforcement and Hasura migration rules are team policy as of 2026-06-20.
Signed stamp image pending re-generation
What it is
The Phenom platform runs two fully separated environments: a staging tier (also called the development tier) and a production tier. They share the same architecture — NEST app on Cloudflare Pages → API (Hasura GraphQL + Auth) on AWS → RDS PostgreSQL — but every layer is a distinct deployment with its own URL, its own database, and its own branch. Nothing in staging touches production data, and nothing reaches production without an explicit, reviewed promotion.
This runbook is the orientation page for the per-asset runbooks. It exists so an engineer can see the whole topology at once, know exactly which URL belongs to which tier, and follow the enforced workflow for shipping a change safely.
Environment URLs
Every Phenom asset, by tier. Use these to confirm you are pointed at the right environment before you deploy, query, or test.
| Layer | Staging / Development | Production |
|---|---|---|
| NEST app | https://dev-nest.thephenom.app |
https://nest.thephenom.app |
| API (Hasura + Auth) | https://api-staging.thephenom.app |
https://api.thephenom.app |
| Database (RDS PostgreSQL) | phenom-dev-postgres (no public endpoint) |
phenom-prod-postgres (no public endpoint) |
| ECS cluster | phenom-dev-cluster |
phenom-prod-cluster |
| Cloudflare Pages project | phenom-backend-dev |
phenom-backend-prod |
| Source branch | develop |
main |
| Cognito user pool | us-east-1_knEL7cqS3 (shared) |
us-east-1_knEL7cqS3 (shared) |
| Public website | — | https://www.thephenom.app |
| Chat (Synapse) | — | https://chat.thephenom.app |
| Analytics | https://analytics.thephenom.app (reads dev DB) |
— |
Cognito is shared. Both tiers authenticate against the same Cognito pool (
us-east-1_knEL7cqS3). A user account therefore exists in both environments; only the data behind the API differs. Treat auth as cross-environment and never assume a staging login is isolated from production identity.Databases are private. Neither RDS instance has a public endpoint. All SQL access goes through the
phenom-oneoff-sqlECS Fargate task inside the VPC — see the RDS dev and RDS prod runbooks.
How each environment works
Staging / development tier
The staging tier is where all integration testing, QA, and pre-production verification happens.
dev-nest.thephenom.appis built by Cloudflare Pages from thedevelopbranch ofPhenom-Backend. Every push todeveloptriggers an automatic build.- It calls the staging API at
api-staging.thephenom.app, which runs the same Hasura GraphQL + Auth stack as production but against thephenom-dev-postgresdatabase. - Staging runs on the
phenom-dev-clusterECS cluster and stores non-production data only.
Full detail: Dev NEST runbook · API staging runbook · RDS dev runbook.
Production tier
The production tier serves real users and is the source of truth for all platform data.
nest.thephenom.appis built by Cloudflare Pages from themainbranch ofPhenom-Backend. Merging tomaintriggers an automatic production build.- It calls the public API at
api.thephenom.app, backed by thephenom-prod-postgresdatabase (Multi-AZ, automated backups). This is a P0 data store. - Production runs on the
phenom-prod-clusterECS cluster.
Full detail: NEST runbook · API production runbook · RDS prod runbook.
Topology
flowchart TB
subgraph STAGING["Staging / development tier"]
direction TB
DN["dev-nest.thephenom.app<br/>(CF Pages · develop branch)"]
AS["api-staging.thephenom.app<br/>(Hasura + Auth · phenom-dev-cluster)"]
DDB[("phenom-dev-postgres<br/>(non-prod data)")]
DN --> AS --> DDB
end
subgraph PROD["Production tier"]
direction TB
N["nest.thephenom.app<br/>(CF Pages · main branch)"]
AP["api.thephenom.app<br/>(Hasura + Auth · phenom-prod-cluster)"]
PDB[("phenom-prod-postgres<br/>(source of truth · P0)")]
N --> AP --> PDB
end
COG["Cognito pool us-east-1_knEL7cqS3 (shared)"]
COG -.auth.-> AS
COG -.auth.-> AP
STAGING ==>|"PR: develop → main (reviewed)"| PROD
Enforced workflow
These rules are mandatory. They keep the environment separation real instead of aspirational.
Branch policy
The Phenom-Backend repository has exactly two long-lived branches: develop (development / staging) and main (production). There is no prod or master branch.
- All work is done off the development (
develop) branch, never directly onmain. Engineers cut feature branches fromdevelop, then merge them back intodevelopvia PR. - Production is advanced only by merging
develop→mainthrough a reviewed GitHub Pull Request. Never push directly tomain; themain-merge is what triggers the production deploy, so it is the audit trail and the human gate. - A change must be validated on staging (
dev-nest/api-staging) before the promotion PR is opened.
# Day-to-day: feature branch → develop (auto-deploys to dev-nest / staging)
git checkout develop && git pull
git checkout -b feature/<issue>-<short-desc>
# ...work...
git push origin feature/<issue>-<short-desc> # open PR into develop
# Promote to production: develop → main via reviewed PR (never a direct push)
gh pr create --base main --head develop \
--title "Release: $(date +%Y-%m-%d)" \
--body "Production release — validated on dev-nest/staging"
# After approval + merge to main, Cloudflare Pages builds nest.thephenom.app automatically.
Branch names are verified against the repo.
develop→ staging andmain→ production are the actual branches and the names wired into the Cloudflare Pages deploy triggers. If a branch is ever renamed, the Cloudflare Pages production-branch setting must be updated in lockstep, or deploys will stop firing.
Environment enforcement
- Development builds use the staging environment only.
dev-nestmust pointNEXT_PUBLIC_API_URLathttps://api-staging.thephenom.app, which is backed byphenom-dev-postgres. A development build must never read or write the production database. - Only production uses the production environment.
nestpoints athttps://api.thephenom.app→phenom-prod-postgres. Nothing else may target the production API or production database for routine work. - Verify the API binding in the Cloudflare Pages environment variables for the matching project (
phenom-backend-devvsphenom-backend-prod) before promoting.
Hasura migrations
Every new Hasura migration must satisfy both of these rules before it is merged:
- Idempotent. Re-running the migration must be safe. Use
IF NOT EXISTS/IF EXISTSguards,CREATE OR REPLACE, andADD COLUMN IF NOT EXISTS, so applying it twice (or applying it after a partial failure) never errors. - Includes the table metadata. A migration that creates or alters a table must also ship the corresponding Hasura metadata — track the table, plus its relationships and permissions — so staging and production stay structurally identical. A schema change without its metadata is incomplete.
Apply to staging first, verify, then to production — never the reverse:
cd hasura/
# 1) Staging first
hasura migrate apply --endpoint https://api-staging.thephenom.app \
--admin-secret "$HASURA_ADMIN_SECRET_STAGING" --database-name default
hasura metadata apply --endpoint https://api-staging.thephenom.app \
--admin-secret "$HASURA_ADMIN_SECRET_STAGING"
# 2) Verify on staging (dev-nest + a test query), then production
hasura migrate apply --endpoint https://api.thephenom.app \
--admin-secret "$HASURA_ADMIN_SECRET" --database-name default
hasura metadata apply --endpoint https://api.thephenom.app \
--admin-secret "$HASURA_ADMIN_SECRET"
Example of an idempotent, metadata-complete migration shape:
-- up.sql — safe to re-run
CREATE TABLE IF NOT EXISTS public.sighting_report (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
reporter_id uuid NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE public.sighting_report
ADD COLUMN IF NOT EXISTS reviewed boolean NOT NULL DEFAULT false;
Then ensure the table is tracked and its permissions/relationships are present in Hasura metadata (metadata/databases/default/tables/) and applied with hasura metadata apply.
Infrastructure changes (Terraform in phenom-infra)
All infrastructure must be defined as code in the phenom-infra repository using Terraform. No infrastructure is created, changed, or deleted by hand in the AWS or Cloudflare consoles. Console click-ops produces drift that Terraform will later try to revert, causes staging and production to diverge silently, and leaves no review trail. If you find a resource that was created manually, import it into Terraform and reconcile — don’t leave it out-of-band.
What “infrastructure” covers here: VPC and networking, ECS Fargate clusters and services, the ALB and target groups, RDS PostgreSQL instances, ECR repositories, AWS Secrets Manager entries, IAM roles, Cognito, WAF/security rules, and the Cloudflare resources that are codified (cloudflare-*.tf). Everything the Phenom stack runs on lives in phenom-infra.
Repository layout — environments are separate Terraform roots that consume shared modules, which is what keeps staging and production isolated yet structurally identical:
phenom-infra/
├── environments/
│ ├── development/ # staging tier root — backend.tf (remote state), main.tf, locals.tf, *.tf per service
│ └── production/ # production tier root — same shape, plus cognito.tf, ecs.tf, secrets.tf, security-waf.tf, cloudflare-*.tf
└── modules/ # reusable building blocks consumed by BOTH environments
├── networking/ # VPC, subnets, NAT, security groups
├── alb/ # load balancer + target groups
├── ecs/ # Fargate cluster + services
├── rds/ # PostgreSQL
├── drop/ # Phenom Drop media pipeline
├── chat-*/ # Synapse / Hasura-lite / MCP chat stack
└── ci-cd/ # pipeline resources
Workflow — change the module or environment config, plan, get it reviewed, then apply. Develop in the development environment first; apply to production only after it is proven in development.
# 1) Make the change in modules/ and/or the relevant environments/ root, on a feature branch.
# 2) Plan against development (staging) FIRST and review the diff.
cd environments/development
terraform init
terraform plan # review every add/change/destroy carefully
# 3) Open a PR with the plan output. Apply development after approval.
terraform apply
# 4) Promote the same change to production only after development is verified.
cd ../production
terraform init
terraform plan # production CI (.github/workflows/prod-infra-ci.yml) also gates this
terraform apply
Rules that make the policy enforceable:
- No console click-ops. Every resource is authored in Terraform. Manual changes are drift and must be imported back into code.
- Modules are the unit of reuse. New infrastructure is added as (or to) a module under
modules/, then referenced from each environment root — never copy-pasted betweendevelopmentandproduction. - State is remote. Each environment’s
backend.tfconfigures remote state; never run Terraform with local-only state. - Production is gated by CI. Changes to the production root flow through
.github/workflows/prod-infra-ci.yml; review the plan before apply. developmentbeforeproduction. Prove an infra change in the development environment, then promote the identical change to production.
Verify the separation is intact
# Staging app is live and talking to the staging API
curl -si https://dev-nest.thephenom.app | head -n 1 # Expect: HTTP/2 200
curl -sf -X POST https://api-staging.thephenom.app/v1/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ __typename }"}' | grep __typename
# Production app is live and talking to the production API
curl -si https://nest.thephenom.app | head -n 1 # Expect: HTTP/2 200
curl -sf -X POST https://api.thephenom.app/v1/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ __typename }"}' | grep __typename
Common failure modes
| Symptom | Likely cause | Remediation |
|---|---|---|
| Dev build hits production data | dev-nest API URL points at api.thephenom.app |
Fix NEXT_PUBLIC_API_URL in the phenom-backend-dev CF Pages env to api-staging.thephenom.app |
| Change reached production unreviewed | Direct push to main branch |
Enforce PR-only merges into main; revert and re-promote via PR |
| Infra changed but not in code | Manual AWS console edit (click-ops) | Import/reconcile into Terraform in phenom-infra; never leave drift |
| Migration fails on second apply | Migration not idempotent | Add IF NOT EXISTS / CREATE OR REPLACE guards; re-author and re-apply staging-first |
| Table exists but GraphQL can’t see it | Migration shipped without metadata | Track the table and apply hasura metadata apply to both envs |
| Staging diverged from prod | Migration/metadata applied to only one env | Apply pending migrations + metadata to staging first, then prod |
develop build not updating |
CF Pages trigger out of sync after a branch rename | Update the Cloudflare Pages production-branch setting to match |
Related
- Runbooks index
- Dev NEST runbook · NEST runbook
- API staging runbook · API production runbook
- RDS dev runbook · RDS prod runbook
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.