03 — Deployment & Management
Deploy target
Section titled “Deploy target”| Service | Platform | Runtime | Region |
|---|---|---|---|
adventive-admin-api | Cloudflare Workers | Cloudflare edge (global) | Smart placement — no pin needed |
adventive-admin-ui | Cloudflare Pages | Static SPA + CDN | Global |
adventive-admin-api (legacy DB) | Cloudflare Hyperdrive | MySQL connection pooler | Collocated with DB region |
Production URLs (to be provisioned):
- API:
https://admin-api.adventive.com(Workers custom domain) - UI:
https://admin.adventive.com(Pages custom domain) - Legacy admin (during transition):
https://admin-legacy.adventive.com(existing EC2)
Both new services sit behind Cloudflare Access. Access policy enforces operator authentication before a single byte of the API or UI is reachable.
Environments
Section titled “Environments”| Env | API URL | UI URL | Trigger | Notes |
|---|---|---|---|---|
| Local | http://localhost:8787 | http://localhost:5173 | wrangler dev / vite dev | Uses .dev.vars for secrets; proxies to Hyperdrive staging binding |
| Preview | admin-api-<branch>.adventive-admin.workers.dev | CF Pages preview URL (per PR) | PR opened or commit pushed to non-main branch | Automated — CF Pages creates preview per commit; Worker preview via wrangler deploy --env preview |
| Staging | admin-api-staging.adventive.com | admin-staging.adventive.com | Merge to staging branch | Shares staging DB with legacy admin-staging; all Cloudflare Access policies active |
| Production | admin-api.adventive.com | admin.adventive.com | Merge to main branch after staging sign-off | Requires explicit manual promotion step — no auto-deploy to prod |
Promotion rules
Section titled “Promotion rules”feature branch → PR → preview deploy (automatic) ↓ review + approve staging branch → staging deploy (automatic on merge) ↓ operator acceptance sign-off (cohort checklist) main → production deploy (manual workflow_dispatch trigger)Production deploys require: (1) staging deploy succeeded, (2) operator checklist signed off, (3) manual trigger by Jeffrey or Patrick.
Production gate
Section titled “Production gate”No wrangler deploy --env production of either the admin-api Worker
or the admin-ui Pages/Worker proceeds until the AWS Secrets Manager
IdP-recovery implementation checklist at
src/content/docs/platform/aws/secrets-manager/01-idp-recovery.md §6
is complete and verified. Dev and stg are exempt; this gate applies
only at prd promotion. Per canonical CLAUDE.md hard rule §3.9.
Build & deploy pipeline
Section titled “Build & deploy pipeline”admin-api (Cloudflare Worker)
Section titled “admin-api (Cloudflare Worker)”File: .github/workflows/deploy-api.yml
name: Deploy admin-api
on: push: branches: [staging, main] workflow_dispatch: inputs: environment: description: 'Target environment' required: true default: staging type: choice options: [staging, production]
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: npm cache-dependency-path: admin-api/package-lock.json - run: npm ci working-directory: admin-api - run: npm run type-check working-directory: admin-api - run: npm run test working-directory: admin-api
deploy: needs: test runs-on: ubuntu-latest environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: npm cache-dependency-path: admin-api/package-lock.json - run: npm ci working-directory: admin-api - name: Deploy to Cloudflare Workers uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} workingDirectory: admin-api command: deploy --env ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}Local development:
cd admin-apicp .dev.vars.example .dev.vars # fill in staging secretswrangler dev --env staging # proxies to staging HyperdriveManual hotfix deploy:
cd admin-apiwrangler deploy --env staging # stagingwrangler deploy --env production # production (requires CLOUDFLARE_API_TOKEN in local env)admin-ui (Cloudflare Pages)
Section titled “admin-ui (Cloudflare Pages)”File: .github/workflows/deploy-ui.yml
name: Deploy admin-ui
on: push: branches: [staging, main] pull_request: branches: [staging, main]
jobs: build-and-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: npm cache-dependency-path: admin-ui/package-lock.json - run: npm ci working-directory: admin-ui - run: npm run type-check working-directory: admin-ui - run: npm run build working-directory: admin-ui env: VITE_API_BASE_URL: ${{ github.ref == 'refs/heads/main' && 'https://admin-api.adventive.com' || 'https://admin-api-staging.adventive.com' }} - name: Deploy to Cloudflare Pages uses: cloudflare/pages-action@v1 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} projectName: adventive-admin-ui directory: admin-ui/dist gitHubToken: ${{ secrets.GITHUB_TOKEN }} branch: ${{ github.ref_name }}Local development:
cd admin-uicp .env.example .env.local # VITE_API_BASE_URL=http://localhost:8787npm run devOpenAPI type generation
Section titled “OpenAPI type generation”Run after any admin-api schema change, before committing admin-ui changes:
# From repo rootcd admin-api && npm run generate:openapi # emits openapi.jsoncd ../admin-ui && npm run generate:types # runs openapi-typescript against openapi.jsonThe generated admin-ui/src/api/generated.ts is committed to source. A CI check validates it is not stale.
Secrets & configuration
Section titled “Secrets & configuration”Rule: No secrets in source. No secrets in wrangler.toml. All environment-specific values use Wrangler secrets (Worker) or Cloudflare Pages environment variables (UI).
admin-api secrets
Section titled “admin-api secrets”| Secret name | Description | Set via | Rotation |
|---|---|---|---|
HYPERDRIVE_DATABASE_URL | MySQL DSN (injected by Hyperdrive binding — not a manual secret) | wrangler.toml binding | Rotated at DB credential rotation |
STRIPE_SECRET_KEY | Stripe API secret key (restricted key — invoice read, customer read only) | wrangler secret put STRIPE_SECRET_KEY | Annually or on compromise |
CF_ACCESS_AUD | Cloudflare Access Application AUD tag (JWT audience validation) | wrangler secret put CF_ACCESS_AUD | On Access Application re-creation |
CF_ACCESS_TEAM_DOMAIN | e.g. adventive.cloudflareaccess.com | wrangler secret put CF_ACCESS_TEAM_DOMAIN | On Access team domain change |
RBAC_SUPER_ADMIN_EMAILS | Comma-separated list of super-admin email addresses | wrangler secret put RBAC_SUPER_ADMIN_EMAILS | When operator list changes |
RBAC_BILLING_EMAILS | Comma-separated list of billing-tier email addresses | wrangler secret put RBAC_BILLING_EMAILS | When operator list changes |
R2_OVERRIDE_BUCKET | R2 bucket name for override files (injected by R2 binding — not a manual secret) | wrangler.toml binding | N/A |
Setting secrets (run once per environment):
wrangler secret put STRIPE_SECRET_KEY --env stagingwrangler secret put STRIPE_SECRET_KEY --env production# etc. for each secretLocal dev secrets (.dev.vars, never committed):
STRIPE_SECRET_KEY=sk_test_...CF_ACCESS_AUD=...CF_ACCESS_TEAM_DOMAIN=adventive.cloudflareaccess.comRBAC_SUPER_ADMIN_EMAILS=jeffrey@adventive.com,patrick@adventive.comRBAC_BILLING_EMAILS=...admin-ui environment variables
Section titled “admin-ui environment variables”UI has no secrets — it only knows the API base URL. Set in Cloudflare Pages dashboard:
| Variable | Staging value | Production value |
|---|---|---|
VITE_API_BASE_URL | https://admin-api-staging.adventive.com | https://admin-api.adventive.com |
Legacy secret migration (Phase 0 prerequisite)
Section titled “Legacy secret migration (Phase 0 prerequisite)”application/config/adventive.php contains production AWS keys (AKIAIAPRP6JG4QQK3YZA), S3 secret, and BitBucket OAuth credentials. These must be rotated and moved to AWS Secrets Manager before any other work begins. This is a blocker, not a Phase 1 task.
Rotation checklist (Phase 0):
- Generate new AWS access key pair — deactivate old key in IAM console
- Generate new Bitbucket OAuth credentials — revoke old tokens
- Move all values to AWS Secrets Manager (or Cloudflare secrets if already accessible from the EC2 host)
- Update CodeIgniter config to read from environment / Secrets Manager at runtime
- Purge
adventive.phpsecret values from source - Rewrite git history to remove committed secrets (
git filter-repoor BFG Repo Cleaner) - Invalidate all existing CI artifacts that may cache the old commit
Cloudflare Access configuration
Section titled “Cloudflare Access configuration”Application definitions
Section titled “Application definitions”| Application name | URL pattern | Policy | Session duration |
|---|---|---|---|
Adventive Admin UI | https://admin.adventive.com/* | Adventive Operators group | 8 hours |
Adventive Admin API | https://admin-api.adventive.com/* | Adventive Operators group + service token (for E2E) | 8 hours |
Adventive Admin UI (Staging) | https://admin-staging.adventive.com/* | Adventive Operators group | 8 hours |
Adventive Admin API (Staging) | https://admin-api-staging.adventive.com/* | Adventive Operators group + service token | 8 hours |
Policy — Adventive Operators group
Section titled “Policy — Adventive Operators group”Rule: Emails → [list of ~10 operator email addresses]Identity provider: JumpCloud (or Google Workspace — deferred per ADR)Require MFA: YesService token (for E2E / Playwright automation)
Section titled “Service token (for E2E / Playwright automation)”Playwright tests authenticate to Access using a CF Access service token:
# Create the service token (one-time, per environment)# Cloudflare dashboard: Access → Service Auth → Create Service Token# Store client_id and client_secret in GitHub Actions secrets# CF_ACCESS_CLIENT_ID, CF_ACCESS_CLIENT_SECRETPlaywright config sets CF-Access-Client-Id and CF-Access-Client-Secret headers in all requests.
Adding a new operator
Section titled “Adding a new operator”- Navigate to Cloudflare Zero Trust dashboard → Access → Access Groups → Adventive Operators
- Add the operator’s email address to the email list
- Notify operator of the new admin URL and confirm they can authenticate
- If adding a super-admin or billing-tier operator, also update
RBAC_SUPER_ADMIN_EMAILSorRBAC_BILLING_EMAILSWrangler secrets
Revoking operator access
Section titled “Revoking operator access”- Remove the operator’s email from the Access group
- If the operator has billing or super-admin RBAC tier, remove from the corresponding Wrangler secret and redeploy the Worker (
wrangler deploy --env production) - Revoke any active Access sessions: Cloudflare dashboard → Access → Revoke User Sessions → enter email
Observability
Section titled “Observability”Workers Analytics Engine
Section titled “Workers Analytics Engine”The admin-api Worker emits structured events to Workers Analytics Engine on every request:
// Captured per requestenv.ANALYTICS.writeDataPoint({ blobs: [route, method, operatorEmail, rbacTier], doubles: [responseStatus, durationMs], indexes: [route]});Dimensions available: route, HTTP method, operator email, RBAC tier, response status, request duration.
Dashboards (to be built in Cloudflare Analytics or Grafana):
- Request rate by route (operator activity heatmap)
- p50/p95/p99 duration by route
- Error rate (4xx, 5xx) by route
- Operator session activity (login frequency, last seen)
Logpush
Section titled “Logpush”Configure Logpush to forward Workers logs to the existing log sink (same destination as Public API Worker):
Cloudflare dashboard → Analytics → Logpush → Create jobDataset: Workers trace eventsDestination: [existing log sink — S3 or Splunk]Filter: worker name = adventive-admin-apiLog entries include: timestamp, outcome (ok/exception/exceededCpu), cpuTime, wallTime, request URL, response status, CF-Ray ID.
Tail Workers (real-time debug)
Section titled “Tail Workers (real-time debug)”wrangler tail adventive-admin-api --env productionwrangler tail adventive-admin-api --env staging --format prettyUse tail Workers during cohort migrations to watch request patterns live.
Alerting
Section titled “Alerting”| Alert | Threshold | Channel |
|---|---|---|
| Worker error rate > 5% (5-min window) | > 5% of requests returning 5xx | PagerDuty / Slack #admin-alerts |
| Worker CPU time approaching limit | p95 > 30ms (warning), > 45ms (critical) | Slack #admin-alerts |
| Hyperdrive connection failures | Any sustained DB error (> 3 consecutive failures) | PagerDuty |
| Access auth failures spike | > 10 failed Access checks in 5 minutes | Slack #admin-alerts |
| Pages build failure | Any failed build on main or staging | Slack #deploy-notifications |
Status page
Section titled “Status page”Add the admin Worker to the existing Adventive status page. External health check endpoint: GET /health → 200 {"status":"ok","version":"x.y.z"}.
Cost model
Section titled “Cost model”All costs are on top of existing Cloudflare account (Workers Paid plan assumed — consistent with Public API project).
| Cost driver | Unit | Estimated monthly usage | Estimated monthly cost |
|---|---|---|---|
| Workers Paid plan | $5/month flat | — | $5.00 |
| Worker requests (admin-api) | $0.30 / million (after 10M free) | ~50K requests/month (~10 operators × ~200 req/day × 25 days) | ~$0 (well within free tier) |
| Worker CPU time | $0.02 / million ms (after 30M free) | ~500K ms/month | ~$0 |
| Cloudflare Pages | Free (unlimited builds, 500 builds/month included) | ~50 builds/month | $0 |
| Hyperdrive | Included with Workers Paid | — | $0 (first 1M queries/month free) |
| R2 storage (override files) | $0.015/GB/month | < 1 GB | < $0.02 |
| R2 operations | $0.36 / million Class B | < 10K/month | < $0.01 |
| Workers Analytics Engine | Free (included in Workers Paid) | — | $0 |
| Logpush | Free for Workers | — | $0 |
Estimated total incremental cost: ~$5–6/month (dominated by Workers Paid plan flat fee, already paid for Public API).
Cost ceiling: At 10 operators with heavy usage (1,000 req/day each), monthly requests = 250K — still well within free request tier. This service will not require re-architecture based on cost at any realistic operator scale.
Rollback procedures
Section titled “Rollback procedures”Worker rollback (admin-api)
Section titled “Worker rollback (admin-api)”Cloudflare Workers retains the 10 most recent deploys. Roll back in under 60 seconds:
# List available versionswrangler deployments list --env production
# Roll back to a specific versionwrangler rollback [deployment-id] --env production
# Or roll back to the previous version (no ID needed)wrangler rollback --env productionVerify rollback: curl https://admin-api.adventive.com/health → check version field.
Pages rollback (admin-ui)
Section titled “Pages rollback (admin-ui)”# Via Cloudflare dashboard:# Pages → adventive-admin-ui → Deployments → click previous deployment → "Rollback to this deployment"
# Or via Wrangler CLI:wrangler pages deployment list --project-name adventive-admin-uiwrangler pages deployment rollback [deployment-id] --project-name adventive-admin-uiCohort rollback (operator routing)
Section titled “Cohort rollback (operator routing)”During the parallel-run period, rolling a cohort back to the legacy admin requires only redirecting the operator:
- Send the operator the legacy admin URL (
https://admin-legacy.adventive.com) - No data migration needed — both admins read/write the same DB
- Document the rollback reason in the cohort migration log
- Investigate the failure before re-attempting cohort migration
No code change or deployment is required for a cohort rollback.
Cohort migration playbook
Section titled “Cohort migration playbook”Pre-migration checklist (for each cohort)
Section titled “Pre-migration checklist (for each cohort)”Before notifying any operator:
- Feature parity checklist for the cohort’s workflows is signed off (Jeffrey + Patrick)
- Staging has been dogfooded by Jeffrey for at least 3 days
- All Vitest tests passing in CI
- E2E Playwright tests passing against staging
- Rollback procedure tested: confirm legacy admin URL still works
- Analytics Engine dashboard live and showing staging traffic
- Logpush forwarding verified to log sink
- Cloudflare Access policy verified: each operator on the list can authenticate
- RBAC secrets set correctly: super-admin and billing emails in Wrangler secrets
Cohort 1 migration — Account Management Operators
Section titled “Cohort 1 migration — Account Management Operators”Scope: Workflows 1–12 (account list, detail, create, activate, cancel, user lookup, unlock, settings, service levels, override files, tools)
Target operators: Operators who primarily use account management and do not require billing access.
Migration day procedure:
1. Send Slack DM to cohort operators: "[New Admin] You're being migrated to the new Adventive Admin today. URL: https://admin.adventive.com — log in with your JumpCloud credentials through Cloudflare Access. Your old bookmarks still work at admin-legacy.adventive.com if you need them."
2. Monitor Workers Analytics Engine dashboard for 30 minutes post-notification: - Watch for 4xx or 5xx spikes - Confirm each operator shows up in the request log (first request = Access JWT validated)
3. Stand by in Slack for 2 hours. Answer questions. Fix anything that surfaces.
4. 48 hours later: check-in with operators. Note any reported issues.
5. At 2-week mark with no rollback requests: mark Cohort 1 as stable. Gate to Cohort 2 is open.Rollback trigger: Any operator reports a workflow they cannot complete that they could complete in the legacy admin. Immediately send them to admin-legacy.adventive.com, document the gap, and fix before re-attempting.
Cohort 2 migration — Billing Operators
Section titled “Cohort 2 migration — Billing Operators”Scope: Workflows 13–25 (invoice list, invoice detail, payment, billing profiles, delinquent accounts, revenue history, managed services list/CRUD)
Dependencies: Stripe Billing Transition Phase B must be stable before this cohort migrates. Coordinate timing with that project.
Migration day procedure: Same as Cohort 1. Additional monitoring: watch Stripe API call success rate from admin-api.
Special note: The billing surface is high-risk. Run Cohort 2 alongside the legacy admin for a minimum of 2 weeks, not just 2 weeks total. If Stripe Billing Transition is in the middle of a cutover when this cohort is ready, hold the cohort until Stripe-side is stable.
Cohort 3 migration — Remaining Operators
Section titled “Cohort 3 migration — Remaining Operators”Scope: Workflows 26–34 (campaigns, managed service jobs, override files, tooling, ad types, benchmarks)
Migration day procedure: Same. After 2-week stable window, all operators are on the new admin.
Post-Cohort-3 freeze
Section titled “Post-Cohort-3 freeze”After all operators have been stable on the new admin for 2 weeks:
- Put legacy admin into read-only mode (disable all POST/PATCH routes in CodeIgniter controllers, or add a maintenance banner to the login page)
- Start 30-day freeze window clock
- At day 30: if no rollback requests, proceed to decommission
Decommission checklist (Phase 10)
Section titled “Decommission checklist (Phase 10)”- All operators confirmed on new admin ≥ 30 days with no rollback
- Legacy admin access logs show zero operator sessions in past 14 days
- Cron jobs (
Reporting.phpscheduled deliveries) migrated to Cloudflare Cron Trigger Workers -
application/config/adventive.phpsecrets already rotated (Phase 0 — verify) - BitBucket repo archived (not deleted)
- EC2 instance terminated (coordinate with DevOps)
- DNS record for
admin-legacy.adventive.comremoved - Cloudflare Access application for legacy admin removed
- CI/CD pipeline for legacy repo disabled
Update — 2026-04-30: UI deploy target changed to Worker + Static Assets
Section titled “Update — 2026-04-30: UI deploy target changed to Worker + Static Assets”The UI is no longer deploying as a Cloudflare Pages project. It now deploys as a Worker with Static Assets (assets = { directory = "./dist", not_found_handling = "single-page-application" } in wrangler.toml).
Why: Cloudflare has converged on Workers as the modern unit of deployment for both static and dynamic. A Worker-with-Assets gives us one deployment unit, one wrangler config, and a clean upgrade path when Phase 2 adds server-side routes (OAuth callbacks, edge auth glue, JWT extraction for the typed API client) to the same Worker without changing the deploy target.
Updated deployment matrix
Section titled “Updated deployment matrix”| Service | Platform | Runtime | Region |
|---|---|---|---|
adv-ui-admin-{env} | Cloudflare Workers (Static Assets) | Cloudflare edge (global) | Smart placement |
adv-svc-admin-api-{env} | Cloudflare Workers | Cloudflare edge (global) | Smart placement |
DB_CONSOLE, DB_BILLING, DB_AGGREGATE | Cloudflare Hyperdrive | MySQL connection pooler | Collocated with DB |
Updated env URL matrix
Section titled “Updated env URL matrix”| Env | UI URL | API URL | UI Worker name | API Worker name |
|---|---|---|---|---|
| Local | http://localhost:5173 | http://localhost:8787 | vite dev | wrangler dev --env dev |
| Dev (preview) | genesis-admin.adventive.dev | admin-api.adventive.dev | adv-ui-admin-dev | adv-svc-admin-api-dev |
| Staging | admin.adventivestg.com | admin-api.adventivestg.com | adv-ui-admin-stg | adv-svc-admin-api-stg |
| Production | admin.adventive.com | admin-api.adventive.com | adv-ui-admin-prd | adv-svc-admin-api-prd |
DNS prerequisite (per standing pattern)
Section titled “DNS prerequisite (per standing pattern)”For each Worker custom hostname, pre-create an AAAA record pointing to 100:: proxied through Cloudflare. Cloudflare does not auto-create DNS for Worker route bindings.
| Zone | Records to pre-create |
|---|---|
adventive.dev | genesis-admin AAAA → 100:: proxied; admin-api AAAA → 100:: proxied |
adventivestg.com | admin AAAA → 100:: proxied; admin-api AAAA → 100:: proxied |
adventive.com | admin AAAA → 100:: proxied; admin-api AAAA → 100:: proxied |
Updated CI/CD
Section titled “Updated CI/CD”The UI’s .github/workflows/deploy-preview.yml now runs wrangler deploy --env dev (was wrangler pages deploy). The API’s deploy workflow follows the same pattern. Rollback for the UI is wrangler rollback --env <env> (Workers Versions) — not Pages Deployments.
Pages-specific bits removed
Section titled “Pages-specific bits removed”public/_redirects— superseded bynot_found_handling = "single-page-application"in wrangler config.wrangler pages project create ...— replaced bywrangler deploy(project provisions automatically on first deploy).- Pages Deployments concept — replaced by Workers Versions (with the same shape: per-deploy URLs, atomic promote/rollback).
public/_headers is retained — Workers Static Assets honors the same Pages-format headers file.
Hyperdrive provisioning gap
Section titled “Hyperdrive provisioning gap”Phase 2 dev binds only DB_CONSOLE (Hyperdrive 059838c4abb64a92a4aece2a6a533a29). DB_BILLING and DB_AGGREGATE Hyperdrives are not yet provisioned in any environment. Endpoints that depend on those bindings throw 503 with a clear message until provisioned. Full provisioning needs to land before stg/prd promotion. Owner: platform team / Stripe Billing Transition project.