Skip to content

ADR-0013: Dev subdomain layering — env label at penultimate position across all brands

  • Status: Accepted — implemented 2026-06-06 (admin.dev / api.dev / labs.dev marnissi-holdings.com CNAMEs live → tunnel; dev.quantmods.com Worker binding live 2026-06-06). §CF Access on the dev tier superseded by ADR-0017 (2026-06-06): every dev.* hostname including the public-frontend dev variants now sits behind CF Access. The rest of this ADR — hostname scheme, routing, WAF, DNS ownership — remains authoritative.
  • Date: 2026-06-06
  • Deciders: cloud-architect, cloudflare-expert, oci-expert, finops-agent, secops-agent, marnissi.investments

Context

With two brands (marnissi-holdings.com and quantmods.com) and two environments (dev and prod) now in scope, the hostname scheme must be codified before additional DNS records, CF Access apps, CF Tunnel ingress rules, or CI deploy targets are added. Today only a production-shaped set of hostnames exists: admin.marnissi-holdings.com, api.marnissi-holdings.com, docs.marnissi-holdings.com, labs.marnissi-holdings.com, and marnissi-holdings.com apex (via the website Worker custom domain, per ADR-0011). There is one CF Tunnel named mgh-dev but it carries production-shaped ingress rules — the word "dev" in the tunnel name reflects the envs/dev/ Tofu workspace, not a dev-vs-prod routing split in the hostname space.

As the workspace grows to two brands and a second environment tier becomes necessary for safe iteration on mgh-admin-api and mgh-admin-ui, an undocumented scheme risks DNS sprawl: records added ad hoc, ingress rules that accumulate in config.yml without a clear ownership model, CF Access apps that are misconfigured because the hostname pattern is ambiguous.

The scheme must satisfy these constraints: (1) one wildcard SSL certificate covers an entire brand's dev tier; (2) sorting hostnames alphabetically groups all dev hostnames together, then all prod hostnames; (3) max three DNS labels (excluding the registrable domain) to stay comfortably within CF and browser certificate compatibility; (4) the scheme is symmetric across brands so tooling (CI, Tofu module inputs, Ansible vars) can be parameterised by brand and env without special cases.

Decision

Codify <service>.dev.<brand>.com as the dev-environment hostname scheme for non-apex hostnames across all brands. The environment label always appears at the penultimate label position — immediately above the registrable domain. The full locked hostname matrix is:

Repo Dev hostname Prod hostname
mgh-website dev.marnissi-holdings.com marnissi-holdings.com + www.marnissi-holdings.com
mgh-admin-ui admin.dev.marnissi-holdings.com admin.marnissi-holdings.com
mgh-admin-api api.dev.marnissi-holdings.com api.marnissi-holdings.com
mgh-docs (no dev env — PR previews on *.pages.dev per ADR-0014) docs.marnissi-holdings.com
labs labs.dev.marnissi-holdings.com labs.marnissi-holdings.com
quantmods-website dev.quantmods.com quantmods.com + www.quantmods.com

Apex frontends. The public-facing websites (mgh-website, quantmods-website) serve from their brand apex in prod and from dev.<brand>.com in dev. This three-label maximum applies — there is no www.dev.<brand>.com. The www redirect rule (prod-only, 301 to apex) is not replicated for the dev tier; direct access to dev.<brand>.com is the entry point.

Why penultimate-position env label, not prefix. An alternative scheme such as dev.api.marnissi-holdings.com (env as leading label) groups all services under a dev.* namespace, which looks superficially tidy but breaks wildcard certificate coverage: a certificate for *.dev.marnissi-holdings.com covers api.dev.marnissi-holdings.com but not dev.marnissi-holdings.com (the apex frontend). With env at penultimate position, *.dev.marnissi-holdings.com covers every non-apex dev hostname, and dev.marnissi-holdings.com is an explicit record for the frontend. This is a smaller, cleaner certificate surface. Additionally, prod hostnames are already deployed in the penultimate-neutral form (admin.marnissi-holdings.com, api.marnissi-holdings.com) — inserting dev one level up from the apex is additive and non-breaking.

docs — no dev hostname. mgh-docs is deployed to Cloudflare Pages (ADR-0014). Pages provides free PR-preview deployments at <deployment-id>.<project>.pages.dev — these URLs serve as the "dev" surface for the docs workload. A separately managed docs.dev.marnissi-holdings.com hostname would require its own Pages Custom Domain binding and provide no additional benefit over the auto-generated preview URLs. Reviewers access PR previews via the *.pages.dev link, not a branded dev hostname.

Routing per env tier. Each dev hostname routes the same way as its prod sibling, applying the decisions in ADR-0011 and ADR-0007:

  • Frontends (mgh-website dev apex, mgh-admin-ui dev, quantmods-website dev apex): Cloudflare Worker (mgh-website-dev, mgh-admin-ui-dev, quantmods-website-dev) bound to a per-env OCI Object Storage bucket (mgh-website-dev, mgh-admin-ui-dev, quantmods-website-dev). Worker custom domain binding creates the DNS record; no manual CNAME is added for these hostnames.
  • Backends (mgh-admin-api dev): CF CNAME → mgh-dev Cloudflare Tunnel → dev container on the ARM A1 VPS. Dev tunnel is mgh-dev (ADR-0007, already live). Prod tunnel is mgh-prod (ADR-0007, declared in envs/prod/ composition).
  • labs dev: CF CNAME → mgh-dev tunnel → dev container. Same tunnel path as backend.

Existing dev environment naming hygiene. The OCI bucket today named mgh-website-prod (declared in infra/envs/dev/main.tf:27) is a naming misnomer — it is a resource in the envs/dev/ workspace but carries "prod" in its name. This will be corrected in a dedicated hygiene PR (rename bucket resource, tofu state mv, update CI secret). That rename is out of scope for this ADR; it is recorded here as a known drift item for tracking.

CF Access on the dev tier. CF Access apps are created for the three internal dev hostnames following the ADR-0008 pattern (same MGH Org Google Workspace OIDC group policy, operator-only membership):

Dev hostname Access app name
admin.dev.marnissi-holdings.com mgh-admin-ui-dev
api.dev.marnissi-holdings.com mgh-admin-api-dev
labs.dev.marnissi-holdings.com mgh-labs-dev

dev.marnissi-holdings.com and dev.quantmods.com (public frontends) are not behind Access. mgh-docs has no dev Access app (docs are public, per ADR-0008 rationale; PR previews on *.pages.dev are also public but ephemeral).

CF WAF on the dev tier. The hostname_allowlist in modules/cloudflare-waf/ (ADR-0010) is extended to include all dev-tier hostnames: *.dev.marnissi-holdings.com, dev.marnissi-holdings.com, *.dev.quantmods.com, dev.quantmods.com. This ensures the CF Free Managed Ruleset evaluates traffic to dev endpoints. The prod-only rate-limit rule on api.marnissi-holdings.com (ADR-0010, the single Free-plan rate-limit quota slot) is not replicated for api.dev.marnissi-holdings.com — dev API traffic is operator-only, low-volume, and rate-limiting the dev surface would consume the sole Free quota slot otherwise reserved for prod.

DNS record ownership. CF Worker custom domain bindings (frontend hostnames) are Tofu-managed via cloudflare_workers_custom_domain. Tunnel CNAME records (backend and labs hostnames) are Tofu-managed in modules/cloudflare-dns/ as cloudflare_record resources pointing at <tunnel-uuid>.cfargotunnel.com. The mgh-admin-api dev CNAME and the labs dev CNAME are new records declared in the envs/dev/ composition under the parameterised module call introduced by ADR-0012.

www redirect for quantmods.com. A Single Redirect rule (matching ADR-0006 pattern, not a Page Rule) is added to the quantmods.com zone: www.quantmods.com → 301 https://quantmods.com/$1. Applied to prod-tier only; the dev frontend (dev.quantmods.com) has no www variant.

Consequences

  • Cost (delta vs free tier): $0. DNS records and zone settings are free. Additional CF Workers and buckets per env are within the Always Free tiers (Workers Free: 100,000 requests/day per account across all Workers; OCI Object Storage: Always Free). CF Access Free tier: 50 seats; adding three Access apps for dev hostnames does not increase seat count (same operator users). No new paid surface.

  • Operational surface:

  • Six new DNS records (dev tunnel CNAMEs for backend + labs) and three new CF Access apps declared in infra/envs/dev/. infra-agent implements these after ADR-0012 module refactor is merged.
  • Per-env Worker scripts (mgh-website-dev, mgh-admin-ui-dev, quantmods-website-dev) deployed by CI from the develop branch of each repo (ADR-0015). CI workflow inputs parameterised by env (dev/prod) and brand.
  • cloudflared ingress rules in roles/cloudflared/templates/config.yml.j2 gain two new entries for api.dev.marnissi-holdings.com and labs.dev.marnissi-holdings.com routing to dev containers (different ports or compose service names from their prod siblings on the same VPS).
  • Naming hygiene PR for mgh-website-prod bucket misnomer is a tracked follow-up; infra-agent owns it. Until corrected, the CI workflow for the mgh-website dev deploy must reference the current bucket name.

  • Security posture:

  • CF Access guards all internal dev hostnames. The same MGH Org group policy applies — no weaker policy on dev than prod.
  • Dev frontend buckets carry the same access_type = "ObjectReadWithoutList" posture as prod (ADR-0011). Static assets are world-readable by design; no secrets in the bundle.
  • HSTS includeSubDomains on marnissi-holdings.com (ADR-0006) already covers *.dev.marnissi-holdings.com subdomains. All dev hostnames must serve HTTPS. secops-agent confirms no dev-tier bypass of HSTS.
  • The WAF extension to *.dev.* hostnames means dev traffic gets managed ruleset evaluation — dev is not a WAF-exempt bypass path. This is intentional.
  • secops-agent checklist addition: on every infra PR that adds a new subdomain, verify it appears in the relevant zone's WAF hostname_allowlist and, if internal-facing, has a corresponding CF Access app.

  • Migration path if we revisit: If the dev-env hostname scheme changes (e.g., moving env label to a leading position), update modules/cloudflare-dns/ CNAME records, CF Worker custom domain bindings in Tofu, Access app hostnames, and cloudflared config.yml.j2. CI workflow env input values also change. Estimated effort: one sprint. Application code is unaffected — it does not embed hostnames.

Alternatives considered

Option Why rejected
dev.api.marnissi-holdings.com (env as leading label) A wildcard *.dev.marnissi-holdings.com does not cover dev.marnissi-holdings.com (the apex frontend dev hostname). The apex frontend would need a separate explicit record regardless, and the cert coverage asymmetry is confusing. Penultimate placement allows *.dev.marnissi-holdings.com to cover all non-apex services with a single wildcard.
api-dev.marnissi-holdings.com (env as suffix on service label) Non-composable. Adding a third brand or a third env requires listing every combination. The <service>.dev.<brand>.com scheme is composable by brand and env parameters in Tofu module inputs and CI workflow matrices.
Shared dev/prod hostnames differentiated by CF Access policy Using the same hostname for dev and prod and relying on CF Access rules to gate dev access conflates the routing concern with the authentication concern. Separate hostnames allow separate containers, separate buckets, separate pipeline triggers, and independent deployability of dev vs prod.
CF Environments / Workers Environments for dev/prod split Wrangler Environments can deploy the same Worker script to two named targets (dev and prod) from one wrangler.toml. This handles Worker deployment but does not address tunnel ingress rules, CF Access app configurations, or OCI bucket separation — those require separate Tofu resources regardless. Using <service>.dev.<brand>.com as the hostname surface keeps the split visible in DNS (the canonical authority for routing) rather than buried in Wrangler config.
No dev tier — develop directly against prod Unacceptable risk. Iterating against production infrastructure on a live hostname means every broken deploy is customer-visible. A dev tier is a safety boundary, not a luxury.