Skip to content

ADR-0022: Edge routing, TLS and hostname scheme

  • Status: Accepted
  • Date: 2026-08-17
  • Deciders: El, Lachlan
  • Supersedes: ADR-0014 §4

Context

ADR-0014 §4 settled Caddy and wildcard TLS but not the hostname pattern. Hostnames are close to permanent once issued — they end up in certificates, Caddy config, client bookmarks, links inside Teams threads, and service agreements. Deciding the pattern after the first tenant exists means living with an inconsistency or breaking links.

Decision

  1. Caddy is the reverse proxy and TLS terminator, part of the base box, routing by hostname. Wildcard certificate via the Cloudflare DNS challenge.
  2. Pattern: tenant first, then service{tenant}.{service}.octopodia.com.au. One wildcard on *.app.octopodia.com.au covers every client, so adding a tenant is a DNS record and an Ansible variable. Service-first (app.{tenant}…) fragments certificates per client.
Surface Hostname
BOS Console console.octopodia.com.au
Documentation docs.octopodia.com.au (ADR-0028)
Client review app {tenant}.app.octopodia.com.au
n8n, Postgres no public DNS, ever

Tenant subdomains stay nested under a service label so they cannot clash with the marketing site or the Internal Portal. 3. Metabase is served under the app, not its own hostname: {tenant}.app.octopodia.com.au/insights, proxied by Caddy to that tenant's container. One hostname, one certificate, one bookmark, no second login. Metabase's site URL is templated for the subpath. 4. portal is reserved for the Internal Portal on our domain. Client-facing is app; operator-facing is console. Two systems called "portal" is how a message ends up meaning the wrong one. On a client's own domain, portal. is fine — it is their vocabulary. 5. Client vanity domains are CNAMEs, never redirects. portal.client.com.au{tenant}.app.octopodia.com.au. A redirect reverts the address bar to our domain, defeating the purpose. Caddy issues a certificate for the hostname automatically. Caddy configuration first, DNS second — the hostname must exist in Caddy before the CNAME resolves or the certificate request fails. 6. A vanity domain is additional, never a replacement. We always hold a working name per tenant that does not depend on the client's registrar. 7. Tenant slug is immutable. It appears in DNS, certificates, database names, vault IDs and directory paths. Lowercase, alphanumeric. It does not change if the client rebrands. 8. DNS record creation is automated; deletion never is (ADR-0024). Creation is additive and namespaced; deletion presents to a client as total outage and persists in resolver caches after the mistake is fixed. 9. The Cloudflare API token used by Ansible is scoped to DNS edit on the specific zone — never the global API key. A global key in a playbook can perform the deletion this ADR refuses to automate.

Consequences

Adding a tenant is a DNS record plus an Ansible variable, with no certificate work. Clients have one address and one login regardless of how many surfaces sit behind it. Accepting vanity domains places the client's DNS in our uptime path: a registrar outage or a deleted record takes their portal down and generates a call to us — the service agreement must name the Octopodia address as the supported one. Serving Metabase on a subpath costs some Caddy and Metabase configuration and removes an entire login and navigation problem.

Alternatives considered

  • Service-first hostnames — rejected: fragments certificates per client for no benefit.
  • Metabase on its own subdomain — rejected: a second address and login for the client, when the app already authenticates them and can sign the embed (ADR-0023).
  • HTTP redirect for vanity domains — rejected: the client's branding is lost at the moment they arrive.
  • Vanity domain as the only address — rejected: we must hold a name that does not depend on a third party's DNS.