services/dph-am-stack.md

dph.am Stack — Master Reference

Start here for anything involving *.dph.am (or the handful of qwestly.co/other domains riding the same nginx box). This doc ties together DNS, the router, nginx, the auth layer, the PWA-injection pattern, and certbot into one picture, plus a step-by-step runbook for adding or removing an app. The three detailed docs below cover their own slice in depth — read this one first to know which of them to open.

Doc Covers
../hardware/mac-studio/services/nginx.md The nginx server itself — Homebrew install, running as a root LaunchDaemon, default_server fallback, migration history/gotchas
../hardware/mac-studio/services/nginx-auth-service.md The auth_request login wall, per-app backend list, OpenClaw's trusted-proxy integration
../hardware/mac-studio/services/nginx-pwa-injection.md Turning an app into an installable PWA + injecting custom scripts/styles via sub_filter, served from ~/Work/pwa/<app>/
This doc How all of the above fit together end-to-end, DNS/router/certbot, and the add/remove-app runbook

The full request path

graph TD DNS["Wildcard DNS
*.dph.am → home public IP"] --> Router["Router
forwards 22, 80, 443"] Router --> Nginx["nginx (root LaunchDaemon)
Mac Studio 192.168.1.189
/opt/homebrew/etc/nginx"] Nginx --> Match{"server_name
match?"} Match -->|no match| Fallback["_localhost.conf default_server
serves ~/Work"] Match -->|matched site| Auth{"auth_request
configured?"} Auth -->|yes| AuthSvc["nginx-auth-service :8085
cookie/session check"] AuthSvc -->|valid| Inject["sub_filter injects
manifest/script/style
from ~/Work/pwa/<app>/ via alias"] AuthSvc -->|invalid| Login["redirect to /login"] Auth -->|no, app has own auth
e.g. c-de.dph.am| Inject Inject --> Backend["backend app
(OpenCode, pi-webui, ttyd, etc.)"] style DNS fill:#2d5a27,color:#fff style Router fill:#4a4a1a,color:#fff style Nginx fill:#1a4a7a,color:#fff style AuthSvc fill:#6b3fa0,color:#fff style Backend fill:#8b4513,color:#fff

Why this shape exists: wildcard DNS means any <anything>.dph.am reaches this Mac — there's no DNS gatekeeping per-app. The gatekeeping happens entirely at nginx: a site either gets its own sites-enabled/<name>.dph.am.conf (deliberately exposed, usually behind auth_request) or it doesn't, in which case it silently falls through to the ~/Work file listing. Wildcard reach + selective nginx config, rather than DNS-level allowlisting, is the actual security boundary.

The four pieces, briefly

  1. DNS: *.dph.am wildcard A record → home's public IP. No per-subdomain DNS entries needed — adding a new <name>.dph.am app never requires a DNS change, only an nginx conf.
  2. Router: ports 22 (SSH), 80 (HTTP, also used for certbot's HTTP-01 challenges), 443 (HTTPS) forwarded to 192.168.1.189 (Mac Studio). This one box is the single ingress point for everything.
  3. nginx: runs as a root LaunchDaemon (sh.brew.nginx, not the default Homebrew user-level agent) so it can read Certbot's root-only certs and bind ports 80/443. sites-enabled/*.conf is the actual routing table — one file per domain. See ../hardware/mac-studio/services/nginx.md.
  4. certbot: issues one individual Let's Encrypt cert per subdomain via the nginx plugin (HTTP-01 challenge, not wildcard/DNS-01) — currently ~33 certs. See the Certbot section below for the Apple-Silicon path bug and the renewal LaunchDaemon.

Layered on top of nginx routing, two optional, independent behaviors per site:

  • Perimeter auth (auth_request → nginx-auth-service) — ../hardware/mac-studio/services/nginx-auth-service.md
  • PWA injection (sub_filter + static assets from ~/Work/pwa/<app>/) — ../hardware/mac-studio/services/nginx-pwa-injection.md

Both are opt-in per sites-enabled/*.conf file — a site can have neither, either, or both.

Certbot specifics

  • Authenticator: nginx plugin, HTTP-01. Not a wildcard cert — every subdomain gets issued and renewed individually. authenticator = nginx / installer = nginx in each /etc/letsencrypt/renewal/<domain>.conf.
  • Account: already registered (account = 7d56dba1eeffab226462bb144eaedc0c in each renewal conf) — never need certbot register again on this box.
  • 🐛 Apple Silicon path bug (fixed 2026-09-20): the certbot nginx plugin defaults to /usr/local/etc/nginx/nginx.conf and binary nginx on $PATH — the Intel Homebrew layout. On this Apple Silicon Mac, nginx actually lives at /opt/homebrew/etc/nginx/nginx.conf / /opt/homebrew/opt/nginx/bin/nginx. Without a fix, every single renewal silently fails (nginx: [emerg] open() "/usr/local/etc/nginx/nginx.conf" failed) — discovered when 0.dph.am's cert was 4 days from expiry with zero renewal infrastructure in place. Fixed permanently via /etc/letsencrypt/cli.ini:
    nginx-server-root = /opt/homebrew/etc/nginx
    nginx-ctl = /opt/homebrew/opt/nginx/bin/nginx
    
    This file is read by every certbot invocation (manual or scheduled) as long as it runs on this machine — no per-command flags needed.
  • Auto-renewal LaunchDaemon: /Library/LaunchDaemons/com.certbot.renew.plist (root, StartCalendarInterval at 03:30 and 15:30 daily), runs:
    certbot renew --quiet --deploy-hook "/opt/homebrew/opt/nginx/bin/nginx -s reload"
    
    The --deploy-hook only fires nginx reload on domains that actually renewed (Let's Encrypt only renews certs within ~30 days of expiry), so this is safe to run twice daily indefinitely. This LaunchDaemon did not exist before 2026-09-20 — the old machine presumably had its own renewal setup that didn't carry over in the migration; verify this exists after any future migration to a new box.
    • Verify it's registered: sudo launchctl print system/com.certbot.renew | grep -i state
    • Logs: /var/log/certbot-renew.log
    • Manually test without touching real rate limits: sudo certbot renew --dry-run

Runbook: adding a new app

Wildcard DNS means no DNS step is ever needed for a new <name>.dph.am. Everything happens in nginx + certbot + (optionally) ~/Work/pwa/.

  1. Decide the shape: does it need perimeter auth (almost always yes, unless the backend has its own real auth — see c-de.dph.am as the precedent for "no")? Does it need PWA treatment (installable icon, injected UX script/style)?
  2. Get a minimal HTTP-only conf running first (certbot's nginx plugin needs to see a server_name block to attach to):
    # /opt/homebrew/etc/nginx/sites-enabled/<name>.dph.am.conf
    server {
        listen 80;
        server_name <name>.dph.am;
        location / { return 404; }
    }
    
    sudo nginx -t && sudo nginx -s reload
  3. Issue the cert — this single command both obtains the cert and rewrites the conf file to add the listen 443 ssl, cert paths, and an HTTP→HTTPS redirect block automatically (same pattern as every existing site):
    sudo certbot --nginx -d <name>.dph.am
    
  4. Flesh out the conf — edit the now-certbot-modified file to add, inside the location / block for port 443:
    • If backend needs perimeter auth: copy the auth_request//login//logout/= /auth blocks from an existing protected site (e.g. dash.dph.am.conf is a clean minimal example) and the actual proxy_pass http://127.0.0.1:<port>; — use 127.0.0.1, not the LAN IP, since nginx and every backend now run on the same box (see the max.dph.am write-up in ../hardware/mac-studio/services/nginx-auth-service.md for why LAN-IP self-reference caused a real outage with OpenClaw's trusted-proxy check).
    • If it needs PWA treatment: mkdir ~/Work/pwa/<name>/, add manifest.json/icon/etc., then add location = /manifest.json { alias /Users/dominick/Work/pwa/<name>/manifest.json; } (or location /_static/ { alias /Users/dominick/Work/pwa/<name>/; } for a directory bucket) plus a sub_filter '<head>' '<head><link rel="manifest" ...>...'; + proxy_set_header Accept-Encoding ""; in the main location block. Full detail in ../hardware/mac-studio/services/nginx-pwa-injection.md.
  5. Test and reload: sudo nginx -t && sudo nginx -s reload
  6. Verify: curl -sI https://<name>.dph.am/ and confirm the app loads (and the login wall triggers, if applicable).

Runbook: removing an app

sudo rm /opt/homebrew/etc/nginx/sites-enabled/<name>.dph.am.conf
sudo certbot delete --cert-name <name>.dph.am --non-interactive
sudo nginx -t && sudo nginx -s reload

If it had PWA assets: rm -rf ~/Work/pwa/<pwa-dir-name>/ (the PWA directory name doesn't always match the domain — e.g. rsx.dph.am used ~/Work/pwa/reasonix/, 0.dph.am/c-de.dph.am both used ~/Work/pwa/opencode/. Check ../hardware/mac-studio/services/nginx-pwa-injection.md's per-app table, or just grep -r <name> /opt/homebrew/etc/nginx/sites-enabled/ before deleting a shared PWA dir, since a couple of domains intentionally reuse another app's PWA folder).

2026-09-20 cleanup, for reference: removed supabase.dph.am, supabase-api.dph.am, rsx.dph.am, printer.dph.am, flowise.qwestly.co, excalidraw-collab.dph.am, dsh.dph.am, draw.dph.am, ccli.dph.am, candidate-dom-local.qwestly.co, ai.dph.am — all unused, along with their certs and (for rsx/dsh) their PWA folders. Down from ~44 certs to 33, and 37 files in sites-enabled/ (including _localhost.conf and default.conf, which aren't per-app sites).

Known gaps / open items (consolidated from the three detail docs)

  • Extract the repeated auth_request/login/logout block into a shared nginx include snippet (currently duplicated across 7 files).
  • Pull the two hardcoded secrets (0.dph.am's Basic Auth header, formerly rsx.dph.am's token — now removed) out of plaintext conf files.
  • Add limit_req rate limiting on /login — these hosts are directly internet-facing via the wildcard.
  • Confirm OpenClaw's trusted-proxy config stays correct if nginx or OpenClaw ever move machines again (loopback-only trust, allowLoopback: true, allowUsers: ["dominick"] — see ../hardware/mac-studio/services/nginx-auth-service.md).