Azure DevOps Architecture β Evergrn
Environment Overview
| Environment | Purpose | App Service | Database | URL |
|---|---|---|---|---|
| dev | Local development, feature work | evergrn-api-dev (B1, Canada Central) |
evergrn-db / user evergrndev (East US 2) |
https://evergrn-api-dev-c7dxhkf3ctcgdqby.canadacentral-01.azurewebsites.net |
| staging | Pre-production validation, QA | evergrn-api-stage (B1, Canada Central) |
evergrn-db-stage / user evergrnstage (East US 2) |
https://staging.evergrn.co (via custom domain) |
| production | Live customer traffic | (not yet provisioned) | (not yet provisioned) | https://evergrn.co |
Resource Groups
| Resource Group | Region | Contents |
|---|---|---|
evergrn-dev |
Canada Central | Dev App Service Plan, App Service, storage accounts (evergrnpkgstore, evergrnuploads), ACS email |
evergrn-stage |
Canada Central | Staging App Service Plan (ASP-evergrnstage), App Service (evergrn-api-stage), PostgreSQL (evergrn-db-stage) |
| (production) | (TBD) | To be cloned from evergrn-stage pattern |
Dev β Staging Flow
Staging is split into two independent services (split 2026-06-28). Each has its own pipeline.
API Deploy (Pipeline ID: 2)
Developer workstation (localhost:3000 API / localhost:5173 Vite)
β
β git push to dev/* branch
βΌ
Azure DevOps repo (evergrn/evergrn)
β
β PR: dev/* β main
βΌ
main branch
β
β Manual trigger: "Run pipeline" in Azure DevOps
βΌ
Pipeline: deploy-staging (pipeline ID: 2)
βββ npm ci (API deps only β no Vite build)
βββ Pre-deploy smoke test against current staging (node scripts/runTests.js --base-url staging)
βββ python make_deploy_zip.py (API only, no client/dist)
βββ az storage blob upload β evergrnpkgstore/deployments
βββ WEBSITE_RUN_FROM_PACKAGE updated on evergrn-api-stage
βββ az webapp restart
βββ Poll /health until 200 (5 min timeout)
βββ Post-deploy test suite against new staging
β
βΌ
API live at https://staging.evergrn.co
Web (SWA) Deploy (Pipeline ID: 4)
main branch
β
β Manual trigger: "Run pipeline" in Azure DevOps (definitionId=4)
βΌ
Pipeline: deploy-staging-web (.azure/pipelines/deploy-staging-web.yml)
βββ npm ci (client deps)
βββ npx vite build --mode staging (sets VITE_API_BASE=https://staging.evergrn.co)
βββ az staticwebapp secrets list β retrieves SWA deployment token dynamically
βββ npx @azure/static-web-apps-cli deploy ./dist --env production
β
βΌ
Web live at https://web.staging.evergrn.co
Staging β Production Flow (not yet automated)
Once staging is validated:
- Create
evergrn-prodresource group (mirroringevergrn-stageSKUs, or upgrade) - Provision production PostgreSQL (
evergrn-db-prod) + App Service (evergrn-api-prod) - Run
prisma migrate deployagainst prod DB - Create pipeline
deploy-production(clone ofdeploy-stagingwith approval gate) - Add human approval step in Azure DevOps before prod deploy executes
- Point
evergrn.coDNS β production App Service custom domain
Azure DevOps Pipelines
Pipeline 1 β API (deploy-staging, ID: 2)
| Item | Value |
|---|---|
| Organization | https://dev.azure.com/evergrn |
| Project | evergrn |
| Repo | evergrn |
| Pipeline name | deploy-staging |
| Pipeline ID | 2 |
| YAML path | .azure/pipelines/deploy-staging.yml |
| Trigger | Manual only (trigger: none) |
| Variable group | evergrn-staging (ID: 2) |
| Service connection | evergrn-azure (ID: 18d541d7-5b30-4a5f-a2ec-7daf84c21f49) |
| Service principal | evergrn-stage-deployer (app ID: 2fb4897d-4f2d-469e-84b6-50aabb519691) |
| SP scope | Contributor on evergrn-stage resource group |
To run a staging API release:
- Go to
https://dev.azure.com/evergrn/evergrn/_build?definitionId=2 - Click Run pipeline
- Confirm (optional: check Skip pre-deploy smoke test to save time)
Pipeline 2 β Web SWA (deploy-staging-web, ID: 4)
| Item | Value |
|---|---|
| Pipeline name | deploy-staging-web |
| Pipeline ID | 4 |
| YAML path | .azure/pipelines/deploy-staging-web.yml |
| Trigger | Manual only |
| Build command | npx vite build --mode staging |
| Deploy target | evergrn-web-staging Azure Static Web App |
| Token retrieval | az staticwebapp secrets list --name evergrn-web-staging --resource-group evergrn-stage |
To run a staging web release:
- Go to
https://dev.azure.com/evergrn/evergrn/_build?definitionId=4 - Click Run pipeline
Deploy Zip Contents
The deploy zip (deploy3.zip, built by make_deploy_zip.py) contains:
| Path | Notes |
|---|---|
server.js |
Entry point |
package.json, package-lock.json |
Dependency manifest |
src/ |
Express app, routes, middleware |
prisma/ |
Schema + migration files |
node_modules/ |
All API dependencies |
client/dist/ is not included in staging or production API deploys β the web app is deployed separately to the Azure Static Web App (evergrn-web-staging). The API zip is API-only in all environments.
Dev deploys (.\deploy.ps1 with no args) deploy the API only. The dev App Service is a pure API.
Staging Architecture β Web/API Split (as of 2026-06-28)
Staging runs as two independent services:
| Service | Resource | URL | What it serves |
|---|---|---|---|
| API | evergrn-api-stage App Service (B1) |
staging.evergrn.co |
Express API β open to internet, no Easy Auth |
| Web | evergrn-web-staging Azure SWA (Standard) |
web.staging.evergrn.co |
React SPA (Vite build) β protected by Entra CA / MFA |
The iOS app calls staging.evergrn.co (API) directly via its own JWT auth flow. The CA policy only gates the web UI.
The web app's Vite build uses --mode staging which sets VITE_API_BASE=https://staging.evergrn.co. A fetch interceptor in client/src/main.jsx prepends this base URL to all relative API calls.
Note on Easy Auth: Easy Auth remains disabled on evergrn-api-stage. The B1 tier causes a VNETFailure when the auth sidecar tries to set up VNet integration. Access control is enforced at the SWA layer instead.
Staging Access Control (Entra ID)
Access control is enforced at the web SWA layer (web.staging.evergrn.co), not at the App Service. Easy Auth is disabled on the API App Service.
| Setting | Value |
|---|---|
| Entra ID App Registration | Evergrn Staging Access |
| Client ID | eb7e1feb-8290-4d6d-ac20-5f71d89da306 |
| Tenant | 93a4d22f-b942-4a6d-a3e6-be71843021a3 |
| CA policy | Require MFA - Evergrn Staging |
| SWA auth provider | Custom OIDC β https://login.microsoftonline.com/93a4d22f-.../v2.0 |
| SWA app settings | ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET |
| Entra license | P1 assigned to kdavis@evergrn.co (required for CA policies) |
| Security Defaults | Disabled (required for CA policies to work) |
Effect: Anyone navigating to web.staging.evergrn.co must complete Entra MFA. The API at staging.evergrn.co is open β the iOS app authenticates via the Express JWT flow directly, which is unaffected by the CA policy.
Custom Domain Setup
To activate staging.evergrn.co, add these DNS records at your registrar, then run the domain binding command:
Step 1 β DNS records (add at registrar):
TXT asuid.staging.evergrn.co B3EB55976204AB5B9897CC92A20C0B46146545E66ABFA1C5B3067DEFB5CB73B2
CNAME staging.evergrn.co evergrn-api-stage.azurewebsites.net
Step 2 β Bind domain (once DNS propagates, ~15 min):
az webapp config hostname add `
--hostname staging.evergrn.co `
--webapp-name evergrn-api-stage `
--resource-group evergrn-stage
# SSL (managed certificate β free)
az webapp config ssl bind `
--name evergrn-api-stage `
--resource-group evergrn-stage `
--hostname staging.evergrn.co `
--ssl-type SNI
Staging Database
| Property | Value |
|---|---|
| Server | evergrn-db-stage.postgres.database.azure.com |
| Admin user | evergrn_admin / 3vergrn! |
| App user | evergrnstage / 3vergrn!Stage (SELECT/INSERT/UPDATE/DELETE only, no DDL) |
| SKU | Standard_B1ms, Burstable, 32 GB |
| Version | PostgreSQL 18 |
| Backups | 7-day retention, no geo-redundancy |
| Access | Azure services + local dev IP (45.46.146.24) |
Schema: All 7 migrations applied at provisioning. For future schema changes:
# Against staging DB:
$env:PGPASSWORD = "3vergrn!"
& "C:\Program Files\PostgreSQL\18\bin\psql.exe" "host=evergrn-db-stage.postgres.database.azure.com port=5432 dbname=postgres user=evergrn_admin sslmode=require" -f migration.sql
# Then grant perms on new tables:
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO evergrnstage;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO evergrnstage;
Variable Group β evergrn-staging
| Variable | Secret | Value |
|---|---|---|
STAGING_API_URL |
No | https://evergrn-api-stage.azurewebsites.net |
STORAGE_ACCOUNT |
No | evergrnpkgstore |
STORAGE_CONTAINER |
No | deployments |
APP_NAME |
No | evergrn-api-stage |
RESOURCE_GROUP |
No | evergrn-stage |
STRIPE_SECRET_KEY |
Yes | (Stripe test mode key) |
DATABASE_URL |
Yes | postgresql://evergrnstage:β¦@evergrn-db-stageβ¦ |
Test Suite Integration
The test suite supports targeting any environment:
# Dev (default)
node scripts/runTests.js
# Staging
node scripts/runTests.js --base-url https://evergrn-api-stage.azurewebsites.net
# Production (future)
node scripts/runTests.js --base-url https://evergrn.co
The pipeline runs it twice: once pre-deploy (smoke test existing staging) and once post-deploy (validate new build).