Appearance
0028 — Per-Portal Subdomain Addressing & TLS ​
Status: Accepted (2026-06-21)
Date: 2026-06-21
ADO work item: Epic AB#3074 (Platform build); Story AB#3675 (domain naming / HCS tenant / Cloudflare); Feature AB#3079 (per-portal subdomains)
Deciders: Kristopher Turner (platform owner)
Amends: ADR 0026 — Production Domain and DNS Strategy, ADR 0008 — Platform Composition
Context ​
ADR 0026 established heritageva.app as the production domain and recorded two DNS records: the apex CNAME (web app) and api. (Container Apps). It stated that Azure-managed certificates handle TLS for all subdomains and assumed the SWA custom-domain flow would cover whatever portal subdomains were needed later.
ADR 0008 described the core web shell as a single apps/web with "navigation, auth-gated routing, layout, and account/profile screens" — implying one shell, one hostname. Neither ADR resolved how the platform's multiple portals (homeschool, marketplace, family, calendar, etc.) are addressed on the web.
Two facts discovered during Platform build invalidate the ADR 0026 subdomain TLS assumption:
- Azure SWA Free and Standard tiers do not support wildcard certificates. SWA Free permits up to 2 custom domains; SWA Standard permits 5–6. Neither issues wildcard (
*.heritageva.app) managed certificates. SWA only accepts Azure-managed certs — BYO/uploaded certificates (e.g. a Let's Encrypt wildcard) are not supported on either tier. - Alternative wildcard solutions are either unavailable or cost-prohibitive. Let's Encrypt and ZeroSSL issue ACME wildcard certificates, but they cannot be uploaded to SWA. Azure Front Door supports wildcard certs but costs ~$35/month — rejected for a ~200-member ministry. SWA Standard tier's extra custom-domain slots still produce only per-name managed certs, not wildcards.
Additionally, two overview documents (ministry-leadership-overview.md, executive_summary.md) referenced portal subdomains under the abandoned base domain heritage-community-hub.com, which is not registered or in use. This ADR corrects the record.
Decision ​
The platform exposes each major portal on its own subdomain of
heritageva.app. All portal subdomains are Cloudflare-proxied CNAMEs pointing at the single Azure SWA. Cloudflare Universal SSL (free, apex + first-level wildcard*.heritageva.app) terminates TLS at the edge. A Cloudflare Origin Rule rewrites theHostheader to the SWA's default hostname before forwarding, so SWA serves regardless of which subdomain was requested. The web SPA readswindow.location.hostnameand mounts the matching portal module.
Subdomain map ​
| Hostname | Portal | TLS / routing |
|---|---|---|
heritageva.app (apex) | App shell — sign-in / register / home | SWA custom domain (Azure-managed cert) or CF-proxied |
www.heritageva.app | — | Cloudflare redirect rule → https://heritageva.app (301) |
api.heritageva.app | API | Azure Container Apps managed cert — unchanged from ADR 0026 |
profile.heritageva.app | Member profile | CF-proxied → SWA |
family.heritageva.app | Family portal | CF-proxied → SWA |
homeschool.heritageva.app | Homeschool Education Portal | CF-proxied → SWA |
marketplace.heritageva.app | Community Marketplace | CF-proxied → SWA |
pony-express.heritageva.app | Pony Express delivery network | CF-proxied → SWA |
listen.heritageva.app | Sermons & Music | CF-proxied → SWA |
calendar.heritageva.app | Calendar / events | CF-proxied → SWA |
groups.heritageva.app | Small Groups & Ministries | CF-proxied → SWA |
members.heritageva.app | Member directory | CF-proxied → SWA |
rideshare.heritageva.app | Ride share & travel | CF-proxied → SWA |
communities.heritageva.app | Sister Communities | CF-proxied → SWA |
announcements.heritageva.app | Announcements | CF-proxied → SWA |
The list is derived from apps/web/src/pages/. Detail pages (e.g. a single sermon, a single listing) remain as paths within the portal's subdomain rather than additional subdomains.
Cloudflare configuration (executed at deploy gate AB#3106) ​
These changes are scripted/runbooked now but executed as live external changes at the deploy gate:
- Universal SSL — verify Cloudflare Universal SSL is active for
heritageva.app(it coversheritageva.app+*.heritageva.appautomatically for any domain on Cloudflare). - CNAME records — one per portal subdomain, Cloudflare-proxied (orange cloud):
CNAME profile <swaDefaultHostname> Proxied CNAME family <swaDefaultHostname> Proxied CNAME homeschool <swaDefaultHostname> Proxied CNAME marketplace <swaDefaultHostname> Proxied CNAME pony-express <swaDefaultHostname> Proxied CNAME listen <swaDefaultHostname> Proxied CNAME calendar <swaDefaultHostname> Proxied CNAME groups <swaDefaultHostname> Proxied CNAME members <swaDefaultHostname> Proxied CNAME rideshare <swaDefaultHostname> Proxied CNAME communities <swaDefaultHostname> Proxied CNAME announcements <swaDefaultHostname> Proxied CNAME www <swaDefaultHostname> Proxied (redirect rule handles 301) - Origin Rule — match
http.host matches "*.heritageva.app"→ setHostheader to<swaDefaultHostname>. This is what makes SWA accept requests for portal subdomains it has not been individually configured with. - www redirect rule —
http.host eq "www.heritageva.app"→ redirect tohttps://heritageva.app(301, preserve path). - Apex + api records — unchanged from ADR 0026;
apirecord stays DNS-only (grey cloud) per Azure Container Apps managed-cert validation requirement.
SWA Bicep stays apex-only ​
Because Cloudflare handles subdomain TLS and proxies all portal subdomains to the SWA default hostname, the SWA Bicep module (infrastructure/modules/swa.bicep) requires no multi-domain changes. It is parameterised for the apex (customDomain=heritageva.app). The comment in the module is updated to document this explicitly.
SPA hostname routing ​
apps/web/src/lib/portal-router.ts exports a resolvePortal(hostname) function that reads window.location.hostname, strips .heritageva.app, and returns a PortalKey. App.tsx calls this on mount and renders the matching portal module. Unrecognized hostnames (including localhost) fall back to the full-app shell.
Amendments to prior ADRs ​
ADR 0026 (Production Domain and DNS Strategy) ​
ADR 0026's "SSL/TLS" section stated: "Azure SWA built-in managed certificate; issued and renewed automatically once the CNAME and TXT validation records are in place." This was correct for the apex domain but did not address subdomains. The claim that "Azure-managed certificates handle TLS for all subdomains" is superseded by this ADR: Cloudflare Universal SSL, not Azure-managed certificates, provides TLS for all portal subdomains. The apex cert from Azure SWA (or Cloudflare Universal SSL, whichever is active) is unaffected. ADR 0026 is amended by this ADR.
ADR 0008 (Platform Composition) ​
ADR 0008's description of the core web shell as "Feature web UIs mount inside this shell" implied a single hostname. This ADR reconciles, not contradicts, that: the single apps/web shell still serves all portals; the subdomain determines which portal section mounts inside the same shell. ADR 0008 is amended by this ADR to reflect that the web shell is hostname-routed.
Alternatives considered ​
| Option | Cost / mo | Wildcard? | Custom domains | Why not chosen |
|---|---|---|---|---|
| Cloudflare Universal SSL + CF proxy (chosen) | Free | Yes (*.heritageva.app) | Unlimited via CF proxy | — chosen |
| Azure SWA Free managed cert | $0 | No | 2 | 2-domain cap; no wildcard |
| Azure SWA Standard managed cert | ~$9 | No | 5–6 | Per-name certs only; still no wildcard; adds recurring cost |
| Let's Encrypt / ZeroSSL wildcard | Free | Yes | n/a | Cannot upload to SWA; would require a self-run TLS-terminating proxy (nginx/Caddy) in front of SWA — adds operational complexity and another Azure resource |
| Azure Front Door + wildcard cert | ~$35 | Yes | Unlimited | Cost-prohibitive for a ~200-member ministry |
| One SWA domain per portal (no wildcard) | $0 + $9/portal/mo | No | — | Multiple SWA instances; duplicated CI/CD; no shared auth session; cost multiplies with portal count |
Consequences ​
Positive ​
- Free wildcard TLS with no per-subdomain configuration or cert renewal — Cloudflare Universal SSL covers all current and future portal subdomains automatically.
- Single SWA instance — no multi-instance operational burden; one deploy pipeline; shared Clerk session works across subdomains (same root domain, Clerk
domainconfig covers it). - Clean portal URLs —
calendar.heritageva.app,listen.heritageva.app, etc., are memorable and meaningful for a ~200-member community. - SWA Bicep unchanged — no Azure-side changes required for subdomain routing; Cloudflare handles it entirely.
Negative / trade-offs ​
- Cloudflare becomes a hard dependency in the TLS path for portal subdomains. If Cloudflare has an outage, subdomain TLS fails. Mitigation: the apex (
heritageva.app) retains an Azure-managed cert as a fallback entry point; monitor Cloudflare status as part of the standard observability runbook. - The Origin Rule approach means SWA cannot enforce strict Host-header validation per subdomain (all portals look the same to SWA). Auth is enforced by Clerk server-side, not by hostname, so this is an acceptable trade-off.
Risks ​
- Shared-session cookie scope — Clerk's session cookie must be scoped to
.heritageva.app(root domain) for the session to work across subdomains. Verify Clerk'sdomainconfiguration at deploy time. - CORS — the API at
api.heritageva.appmust include all portal subdomains in its allowed origins. This is a deploy-time config change; the API's allowed origins list must enumerate or wildcard-match*.heritageva.app. - Cloudflare cache — portal HTML served through Cloudflare's CDN must set
Cache-Control: no-storefor authenticated routes to prevent cross-user cache poisoning. SWA's default headers are acceptable; review at deploy time.
References ​
- ADR 0026 — Production Domain and DNS Strategy — amended by this ADR
- ADR 0008 — Platform Composition — amended by this ADR
- ADR 0027 — Web Frontend & Design-System Tooling
- ADR 0004 — Cloud/hosting stack, CI/CD
- ADR 0024 — Cloud portability & provider abstraction
- docs/internal/architecture/cloudflare-dns-records.md — full DNS runbook
- infrastructure/modules/swa.bicep
- apps/web/src/lib/portal-router.ts
- ADO: Story AB#3675, Feature AB#3079, Epic AB#3074; Cloudflare tasks AB#3679, AB#3680