Skip to main content

Cloudflare

Cloudflare does everything domain-related and hosts every static app. It was chosen for the free tier, the global edge, R2's free terabyte with no egress fees, and Access for gating internal sites.

What runs where​

ProductUsed forNames
DNSZones everymanawarrior.cc (apps) and emaw-app.com (backend). everymanawarrior.com is the legacy marketing domain.
PagesStatic React apps deployed with wrangler pages deploy from GitHub Actionsemaw-website, emaw-react-web-app, emaw-documentation
Workers (static assets + a small proxy)The Admin CRM, because it needs a same-origin /rest/* proxyemaw-admin-dev, emaw-admin
R2Website resource media (audio, video, documents): no egress fees, 1-year edge cachebucket emaw-public, served from r2.everymanawarrior.cc
Tunnel (cloudflared)Exposes the backend without inbound ports; one tunnel per environmenthostnames dev.emaw-app.com, app.emaw-app.com
Access (Zero Trust)Google Workspace login in front of internal sitesdev website, admin CRM (dev and prod), docs
Cache Rules1-year TTL on r2.everymanawarrior.cc

Pages projects​

ProjectRepodevelop branchmain branch
emaw-websiteemaw-websitedevelop.emaw-website.pages.dev → develop-website.everymanawarrior.ccmanual deploy; production custom domain to be everymanawarrior.com at launch
emaw-react-web-appemaw-web-appdevelop-web-app.everymanawarrior.ccapp.everymanawarrior.cc
emaw-documentationemaw-documentationdocs-dev.everymanawarrior.ccdocs.everymanawarrior.cc

All three are Direct Upload projects: the GitHub workflow builds and runs npx wrangler@4 pages deploy <dir> --project-name <project> --branch <develop|main>. The Pages Git integration is not used. Secrets per repo: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID.

Build-time variables for the website come from GitHub repository variables (VITE_SANITY_*, VITE_EMAW_API_BASE_URL, VITE_STRIPE_PUBLISHABLE_KEY, VITE_GOOGLE_CLIENT_ID, VITE_EMAW_APP_URL); the web app and CRM commit their .env.development / .env.production instead.

Workers: the Admin CRM​

emaw-crm-app/wrangler.jsonc deploys dist/ as a Worker with static assets, single-page-app fallback, and run_worker_first: ["/rest/*"]. The Worker (worker/index.ts) forwards /rest/* to API_ORIGIN (injected at deploy from VITE_API_BASE_URL) and copies the Cf-Access-Jwt-Assertion header into X-Cf-Access-Jwt. Everything else is a 404 from the Worker and a static asset otherwise.

EnvironmentWorkerDomainDeploy
developmentemaw-admin-devadmin-dev.everymanawarrior.ccGitHub Action on push to develop
productionemaw-adminadmin.everymanawarrior.ccnpm run deploy:production by hand

Custom domains on Workers create their own DNS records. public/_headers sets no-framing, nosniff, HSTS and referrer policy.

R2: emaw-public​

  • Public bucket behind the custom domain r2.everymanawarrior.cc, with a zone-level Cache Rule for a 1-year TTL and a CORS rule that allows the admin origins to PUT.
  • The backend presigns direct browser uploads (resources/r2.py); the Admin CRM PUTs the file straight to R2; the public API returns the full media.url, so the website never constructs R2 URLs.
  • Keys are immutable. Replacing a file uploads a new key. Never overwrite a published key; the edge cache would keep serving the old bytes for up to a year.
  • Backend env vars: R2_ACCOUNT_ID, R2_BUCKET, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY (API token with Object Read & Write on the bucket), R2_PUBLIC_BASE_URL. Uploads return 503 until all four credentials are set.
  • Setup and troubleshooting: docs/r2-setup.md and resources/README.md.

Tunnels​

Each backend environment has a remotely managed tunnel (configured in the Zero Trust dashboard, not in a config file). The compose tunnel service runs cloudflared tunnel run --token ${TUNNEL_TOKEN}; the token comes from .env and is not passed into the Django containers.

Consequences:

  • gunicorn's port is never published; only the compose network reaches it (--forwarded-allow-ips '*', TRUST_CF_CONNECTING_IP for the real client IP).
  • Running two connectors with the same token (the dev VM and your laptop) makes Cloudflare split traffic between them. Stop the server's stack before you take the dev tunnel; see Backend local development.
  • Stripe webhooks reach a developer's local stack through the tunnel too, which is why the Stripe CLI bridge in payments/README.md targets the tunnel hostname.

Access​

Access applications (Zero Trust → Access → Applications) use the Google Workspace identity provider with the policy emails ending in @everymanawarrior.com:

ApplicationWhy
develop-website.everymanawarrior.ccStaff-only testing of the new site before launch
admin-dev.everymanawarrior.cc, admin.everymanawarrior.ccProtects the CRM database from anyone outside the workspace
docs-dev.everymanawarrior.cc, docs.everymanawarrior.ccInternal documentation

For the admin app the backend additionally verifies the Access JWT (CF_ACCESS_TEAM_DOMAIN, CF_ACCESS_AUD) on every staff endpoint, so an app password alone is never enough. Locally both variables are unset and the check is skipped.

Account access​

Changes in the Cloudflare dashboard (new Pages project, Access policy, DNS record, R2 token) need account access; ask Matt. Day-to-day deploys only need the API token already stored in each repo's GitHub environments.