Raxx · internal docs

internal · gated

Cloudflare Pages runbook

System: cloudflare-pages (getraxx.com landing page + future static surfaces) Owner: sre-agent Last incident: 2026-05-27 (waitlist form hidden — see RCA 2026-05-27-getraxx-waitlist-flag-invisible) Last reviewed: 2026-05-27

How to tell it's broken

Architecture — where the build actually happens

Critical: The deploy-getraxx.yml workflow uses a build-then-upload pattern. CF Pages only hosts the pre-built dist/. It does NOT run npm run build itself.

GitHub Actions runner
  └─ npm ci && npm run build    ← Vite bakes env vars INTO the bundle here
  └─ wrangler pages deploy dist/  ← uploads static files to CF Pages

Cloudflare Pages
  └─ serves the uploaded dist/   ← has no knowledge of original env vars

Consequence: CF Pages project env vars (set via dashboard or API) have zero effect on what ends up in the bundle. The flag values must be present in the GH Actions step shell at build time.

Env vars for feature flags

Feature flags in frontend/getraxx-landing are injected via Vite's define block in vite.config.js. The values Vite reads come from loadEnv(mode, cwd), which reads from the shell environment and any .env files.

Where to set them:

What you want Where to set it
Always-on flag in production builds Add env: block on the Build getraxx landing step in .github/workflows/deploy-getraxx.yml
Secret build-time value Store in Infisical; add a load-vault-secrets step before the build step; inject into env:
Local dev flag override .env.local in frontend/getraxx-landing/ (gitignored)

Do NOT use: CF Pages project env vars for anything that affects bundle content — they are irrelevant for this pattern.

How to diagnose (in order)

  1. Check whether the issue is visible in the live bundle. Download https://getraxx.com/ and search the JS bundle for the feature string (e.g., "opening soon" vs <input). Confirms whether the correct build was deployed.
  2. Check the GH Actions run for the Build getraxx landing step — inspect the env: block for the expected flags.
  3. Check vite.config.js define block — confirm the flag var is listed and maps to env.REACT_APP_FLAG_*.
  4. If the bundle is correct but the site shows the wrong content, check CF Pages deployment list for whether the latest deployment is active (CF may be serving a cached or prior deployment).
  5. If wrangler pages deploy failed, check CF API token validity — token is CF_PAGES_DEPLOY_TOKEN in Infisical at /MooseQuest/cloudflare/.

Known failure modes

Failure mode A: Feature flag gated content invisible after deploy

Symptom: Expected content (e.g., waitlist form) not visible on live site. Bundle contains "opening soon" or equivalent fallback string. hasFlagEnvRef: false in bundle.

Cause: REACT_APP_FLAG_* env var was not set in the GH Actions Build getraxx landing step. The flag evaluated to falsy at Vite build time and the true branch was tree-shaken.

Fix:

- name: Build getraxx landing
  working-directory: frontend/getraxx-landing
  env:
    REACT_APP_FLAG_GETRAXX_WAITLIST: '1'   # add any needed flags here
  run: |
    set -eu
    npm ci
    npm run build

Add the env: block scoped to the build step. Commit, push, merge — the workflow re-runs automatically on push to main paths matching frontend/getraxx-landing/**.

Verification: After deploy, fetch https://getraxx.com/waitlist?_=cachebust and confirm the expected DOM element is present. Check bundle source for the flag string.

Do NOT: Set the flag on the CF Pages dashboard and expect it to appear in the bundle. It will not.

Failure mode B: CF Pages project does not exist / wrangler create fails

Symptom: Ensure CF Pages project (getraxx) step fails. wrangler pages project create returns a non-4905 error.

Cause: CF API token scope missing Pages:Edit, or the account ID is wrong.

Fix: 1. Read CF_PAGES_DEPLOY_TOKEN and CLOUDFLARE_ACCOUNT_ID from vault (/MooseQuest/cloudflare/). 2. Verify token scopes: curl -sS -H "Authorization: Bearer $TOKEN" https://api.cloudflare.com/client/v4/user/tokens/verify 3. If token is missing Pages:Edit, operator must mint a new token with correct scope and update Infisical.

Verification: Re-run the workflow. Ensure CF Pages project (getraxx) step should succeed or print "getraxx project already exists."

Failure mode C: www.getraxx.com not redirecting to apex

Symptom: https://www.getraxx.com returns 200 and serves content directly instead of 301 → https://getraxx.com.

Cause: www.getraxx.com was re-added as a CF Pages custom domain, which bypasses zone-level Dynamic Redirect rules.

Fix:

# Remove www as a CF Pages custom domain
curl -sS -X DELETE \
  -H "Authorization: Bearer $CF_PAGES_DEPLOY_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/pages/projects/getraxx/domains/www.getraxx.com"

The deploy-getraxx.yml workflow includes a Remove www.getraxx.com CF Pages custom domain step that runs idempotently on every deploy — re-running the workflow is usually sufficient.

Verification: curl -sI https://www.getraxx.com should return 301 with Location: https://getraxx.com/.

See also: docs/ops/incidents/2026-05-11-www-getraxx-access-bypass.md

Failure mode D: CF Access gate still blocking getraxx.com after removal

Symptom: getraxx.com returns a CF Access login wall instead of the public landing page.

Cause: CF Access policy for getraxx.com was not removed or is being re-applied.

Fix: Follow docs/ops/runbooks/getraxx-launch-day-cf-access-removal.md to remove the CF Access application for getraxx.com.

Env vars across Vite + CF Pages + GH Actions deploys

This section explains the full env var chain for any Vite project deployed via deploy-getraxx.yml. Two consecutive SRE incidents (#2844/#2848, #2915) traced to the same misunderstanding: vars set in the CF Pages dashboard had no effect.

Why Vite needs a define: block to expose process.env

Vite is an ES module bundler. It does not expose Node's process.env to client code by default — browser bundles have no process. When Vite encounters process.env.FOO in source code, it replaces process.env with {}, so process.env.FOO evaluates to undefined at runtime.

The fix is a define: block in vite.config.js:

// vite.config.js
import { defineConfig, loadEnv } from 'vite';

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '');  // reads shell env + .env files
  return {
    define: {
      'process.env.MY_FLAG': JSON.stringify(env.MY_FLAG || ''),
    },
  };
});

loadEnv reads from the shell environment (which GH Actions populates via env:) and from .env / .env.local files. The define: block replaces process.env.MY_FLAG at bundle time with the literal string value.

Both steps are required for a new flag: 1. Add the define: entry to vite.config.js — Vite injects the value 2. Set the var in the GH Actions build step env: block — provides the value

Why CF Pages env vars fire at build time only (and only for CF's own builds)

CF Pages supports two deploy patterns:

Pattern Build runs where CF Pages env vars matter?
Direct Upload (this project) GH Actions runner No — CF Pages never runs the build
CF Pages built-in CI CF Pages workers Yes — CF injects them before the build

deploy-getraxx.yml uses Direct Upload: GH Actions runs npm run build, then wrangler pages deploy dist/ uploads the pre-built static files to CF Pages. CF Pages only serves the uploaded dist/ — it has no knowledge of the original env vars and never re-runs npm run build.

Consequence: env vars set on the CF Pages dashboard or via the CF API have zero effect on bundle content for this project.

Why GH Actions env: doesn't auto-propagate to npm run build

A workflow-level env: block is available to all steps, but only if the build step does not override it. Vars set in a step-level env: block take precedence, and any var not listed in the step's env: block that was set at workflow level IS available.

The safest practice is to set flag vars explicitly on the build step:

- name: Build getraxx landing
  working-directory: frontend/getraxx-landing
  env:
    REACT_APP_FLAG_GETRAXX_WAITLIST: '1'   # must live here, not on CF Pages dashboard
  run: |
    set -eu
    npm ci
    npm run build

Secret values that should not appear in plain text in the workflow YAML should be loaded from vault first, then referenced via ${{ env.MY_SECRET }} in the step.

The minimum set of repeats for a new flag

For any new flag variable MY_FLAG:

  1. vite.config.js define block (required — without this, Vite silently drops the var): js 'process.env.MY_FLAG': JSON.stringify(env.MY_FLAG || ''),

  2. GH Actions build step env: block (required — source of truth for prod value): yaml - name: Build getraxx landing env: MY_FLAG: '1'

  3. Local dev (optional — .env.local in the project root, gitignored): MY_FLAG=1

There is no step 4 for CF Pages dashboard. Do not set it there.

Audit tool

scripts/ops/audit_vite_env_defines.py lists all process.env.* references in Vite project source files and cross-checks them against the define: block:

python3 scripts/ops/audit_vite_env_defines.py
python3 scripts/ops/audit_vite_env_defines.py --fail-on-missing   # CI mode

Run this when adding a new flag to catch missing define entries before deploy.

Critical pattern notes — Vite + CF Pages + GH Actions

This project uses a pattern where:

  1. Vite replaces process.env.REACT_APP_* at build time (not runtime). The bundle contains the baked-in value.
  2. CF Pages hosts static files only; it never re-runs the build.
  3. Therefore, all feature-flag env vars MUST be present in the GH Actions shell during npm run build.

Any agent or engineer adding a new feature flag to this project must: 1. Add the var to the define block in vite.config.js 2. Set its value in the env: block of the Build getraxx landing step in deploy-getraxx.yml

Failure to do step 2 will cause the flag to evaluate as falsy in production, even if it appears to be set on CF Pages dashboard.

PR preview — getraxx landing

PRs touching frontend/getraxx-landing/** automatically get a Cloudflare Pages branch preview via .github/workflows/pr-preview.yml.

How it works:

The getraxx job in pr-preview.yml builds the landing page and deploys to the getraxx CF Pages project under a branch-scoped namespace (not the main production branch). The preview branch URL pattern is:

https://<sanitized-branch-name>.getraxx.pages.dev

A per-build URL (unique to the commit) is also posted:

https://<8-hex-chars>.getraxx.pages.dev

Both URLs appear in the sticky PR comment.

Access control: Preview branch URLs on getraxx.pages.dev are gated behind Cloudflare Access (same wildcard policy as raxx-app.pages.dev). First click sends a 6-digit code to kris@moosequest.net. Session lasts 24 hours.

Flag behaviour in previews: REACT_APP_FLAG_GETRAXX_DIFFERENTIATION_SECTION=1 is set unconditionally in the preview build step so reviewers can see the differentiation section even when the production flag is OFF. Other flags (e.g. REACT_APP_FLAG_GETRAXX_WAITLIST) are also ON in previews, matching their production state.

Why the preview uses the getraxx production project (not a separate preview project): CF Pages supports per-branch namespaces within a single project. Previews deploy to branch <pr-head-ref> inside the getraxx project; production always deploys to the main branch. There is no risk of the preview overwriting production because wrangler uses --branch=<branch> (not --branch=main). The CF Pages uniqueness lint (lint-cf-pages-deploy-uniqueness.yml) allowlists pr-preview.yml for exactly this reason.

Failure mode E: getraxx preview build fails with "process.env.REACT_APP_FLAG_* is undefined"

This means a new flag was added to component source but not to vite.config.js define block. Fix: add the define entry to frontend/getraxx-landing/vite.config.js and re-push.

Failure mode F: getraxx preview URL not appearing in PR comment

Check that the PR touches files under frontend/getraxx-landing/. The path filter in pr-preview.yml is frontend/getraxx-landing/**. Changes only to workflow files or other paths will not trigger the getraxx job.

Does not affect production: The deploy-getraxx.yml production workflow deploys only on push to main with --branch=main. PR preview deploys use the PR head branch name. These are independent CF Pages branch deployments.

Emergency stop

To take getraxx.com offline:

# Point the getraxx.com apex CNAME at a maintenance page (or delete the Pages deployment)
# OR re-enable CF Access gate for getraxx.com to require authentication

Consult the operator before any emergency stop — getraxx.com is a public marketing surface.

Escalation

Escalate to operator when: - CF API token needs re-minting (operator has 2FA access) - The getraxx CF Pages project needs to be deleted and recreated - DNS changes outside the getraxx.com zone are needed - A new custom domain needs to be attached to the Pages project