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:

  1. Create evergrn-prod resource group (mirroring evergrn-stage SKUs, or upgrade)
  2. Provision production PostgreSQL (evergrn-db-prod) + App Service (evergrn-api-prod)
  3. Run prisma migrate deploy against prod DB
  4. Create pipeline deploy-production (clone of deploy-staging with approval gate)
  5. Add human approval step in Azure DevOps before prod deploy executes
  6. Point evergrn.co DNS β†’ 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:

  1. Go to https://dev.azure.com/evergrn/evergrn/_build?definitionId=2
  2. Click Run pipeline
  3. 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:

  1. Go to https://dev.azure.com/evergrn/evergrn/_build?definitionId=4
  2. 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).