WHA Docs

Architecture

Directory structure, system diagram, request flow and how all the pieces connect.

System diagram

Request flow

  1. Cloudflare handles DNS and proxying, terminates the public edge
  2. Heroku Router terminates SSL and routes to the dyno
  3. Nginx serves static files directly (JS, CSS, images, fonts) and proxies PHP requests to PHP-FPM
  4. PHP-FPM runs WordPress: index.phpwp/wp-blog-header.php
  5. Amazon RDS (MySQL) stores all WordPress data

wp-admin lives at /wp/wp-admin/. Nginx rewrites bare /wp-admin and /wp-login.php automatically.

Services & integrations

Not everything runs inside the PHP request. The site relies on a few separate services and external APIs, accessed through yax-* plugins so the theme never talks to them directly:

  • Backing services (private infrastructure / cloud). Media is offloaded to S3 (yax-offload-media), search runs against a Meilisearch index (yax-meilisearch, deployed per environment), and operator observability is GlitchTip (errors + performance) and Plausible (cookieless analytics). These are separate deployables the WordPress app reads and writes over the network, not embedded libraries.
  • Inbound data syncs. Provider and location profiles come from Doctor.com (wha-doctorcom-sync), patient ratings from PressGaney (wha-provider-ratings), and the Instagram/social feed is fetched and proxied server-side. These run on WP-Cron and cache into WordPress, so a third-party outage never blocks a page render.
  • Outbound analytics. Named events are sent to FreshPaint (the marketing/conversions stack, which forwards to GA4 and Meta) and to Plausible (the operator lens). See Event Tracking.

The deeper "how each service works" is a property of the yax-* framework, not of WHA specifically; those mechanics live with the ecosystem docs. This page covers only which services WHA actually runs and how they connect.

How this differs from standard WordPress

The wp-admin experience is standard WordPress. Updates, plugin installs and content editing all work normally. The differences are under the hood:

  • Composer manages WordPress core and plugins as dependencies (pinned in composer.lock). Updates through wp-admin are synced back to the manifest automatically by yax-version-sync.
  • Project code lives in src/, separate from Composer-managed code in wp/ and vendor/.
  • Deployment builds a self-contained artifact in dist/ via tools/build-dist.sh. See Deployment.
  • Configuration is entirely environment-driven via wp-config.custom.php. No hardcoded values.
  • Custom plugins (the yax-* framework) fill gaps WordPress doesn't cover natively. See Plugin System.

WordPress configuration

All settings come from environment variables. src/wp-config.php loads wp-config.custom.php, which reads everything via getenv_docker(). URLs are detected dynamically from HTTP_HOST, so the same codebase runs on any domain without config changes. HTTPS is detected through multiple proxy headers (X-Forwarded-Proto, CF-Visitor) to handle the Cloudflare → Heroku → Nginx → PHP-FPM chain.

See Hosting > Environment Variables for the full variable reference.

Custom plugin ecosystem

The site uses two layers of custom mu-plugins alongside standard third-party plugins:

  • yax-* plugins — A shared framework maintained by the site developer, used across multiple projects. These fill gaps in core WordPress: media offloading, Composer version sync, component rendering, fluent query building, security hardening and more. Maintained as Composer packages via a private registry (composer.joeyyax.app).

  • wha-* plugins — Built specifically for WHA. These register the custom post types (providers, locations, services, life stages, etc.) and site-specific business logic like the Doctor.com rating sync. Committed directly to the repo.

See Plugin System for the full list and Custom Post Types for the data model.

Media offloading

Media uploads are offloaded to S3 (handled by the yax-offload-media mu-plugin), which rewrites upload URLs to point at the bucket. Media uploaded through wp-admin gets stored both locally and in S3. Public URLs serve from S3 via Cloudflare.

Nginx

Two Nginx configs exist:

  • src/nginx.conf — Production. Used inside the single-container deployment (PHP-FPM + Nginx together).
  • src/nginx.dev.conf — Development. Proxies PHP to the separate wordpress container.

Both share the same routing rules:

  • Static files served directly with expires max
  • /wp-admin and /wp-login.php redirect to /wp/wp-admin/ and /wp/wp-login.php
  • PHP requests proxied to PHP-FPM with proper X-Forwarded-* headers
  • Sensitive files blocked (., wp-config.php, readme.html and xmlrpc.php)
  • Upload PHP execution blocked (/uploads/*.php denied)

Cookie consent (the yax-cookie-consent mu-plugin) controls when analytics scripts load. Freshpaint's tracking is gated behind consent. The yax_cookie_consent_block_analytics filter prevents analytics from firing until the user accepts.

Cloudflare

Cloudflare sits in front of the site handling DNS, CDN and security. The domain is on the Free plan. See Caching for cache rules and Security for WAF (Web Application Firewall) rules, geo-blocking and Turnstile configuration.

On this page