Skip to content

Custom Domain

Attach your own hostname (www.example.com, app.example.com) to a PaaS app. The platform owns the TLS certificate via cert-manager and routes traffic through the same Ingress that serves the app's auto-provisioned subdomain.

The whole flow is two API calls: POST to register the domain, then POST /verify to flip the row to verified once DNS is in place. Per-app cap: 10 custom domains.

How it works

sequenceDiagram
  participant U as Operator
  participant CP as Control Plane
  participant DB as Postgres (app_domains)
  participant T as Traffic Service
  participant K as Kubernetes (Ingress + cert-manager)
  participant DNS as DNS

  U->>CP: POST /v1/apps/{app}/domains { domain }
  CP->>CP: format check, subdomain guard, count_by_app < 10
  CP->>CP: token = uuid().replace('-', '')
  CP->>DB: INSERT app_domains (verification_token, status='pending_verification')
  CP->>T: add_custom_domain(app, domain)
  T->>K: patch IngressRoute, create cert-manager Certificate
  CP-->>U: 200 + { verification: { CNAME, alternative: TXT } }

  U->>DNS: publish CNAME or TXT record
  U->>CP: POST /v1/apps/{app}/domains/{domain}/verify
  CP->>DNS: dig CNAME (and TXT fallback)
  alt CNAME or TXT matches
    CP->>DB: UPDATE status='verified', verified_at=NOW()
    CP-->>U: 200 { verified: true, cname_ok, txt_ok }
  else no match
    CP-->>U: 200 { verified: false } (still pending — retry later)
  end

Cap: 10 domains per app

Once an app has 10 app_domains rows, the next POST returns 422 quota_exceeded with the message "domain limit (10) reached for this app". Delete an unused domain before adding a new one.

Verification — CNAME (preferred)

The platform creates an auto-subdomain like app-{app_id}.runtime.di2amp.com. Point your custom hostname at it with a CNAME:

www.example.com.   IN CNAME   app-749ddc1d-….runtime.di2amp.com.

Then call:

curl -X POST -H "Authorization: Bearer $TOKEN" \
  https://runtime.di2amp.com/api/v1/apps/$APP/domains/www.example.com/verify

Response:

{
  "data": {
    "domain":   "www.example.com",
    "verified": true,
    "cname_ok": true,
    "txt_ok":   false
  }
}

Verification — TXT (apex / wildcard fallback)

Apex domains (example.com) and wildcards (*.example.com) can't take a CNAME — provide a TXT record at _paas-verify.<domain> whose value contains the token returned at POST time:

_paas-verify.example.com.   IN TXT   "paas-verify=4f3a8e9b…"

The verify endpoint runs CNAME first, then TXT only when CNAME has failed AND a token is persisted. Either path flips the row to verified.

TLS

The traffic service registers a cert-manager Certificate resource for each new custom domain at POST time, so the moment DNS resolves the cert is issued (Let's Encrypt HTTP-01) and the app starts serving HTTPS. No additional API call needed.

API endpoints

Verb Path Body Notes
GET /v1/apps/{id}/domains list per app, sorted by created_at
POST /v1/apps/{id}/domains { "domain": "www.example.com" } 422 if cap; returns verification block
POST /v1/apps/{id}/domains/{domain}/verify DNS check; idempotent
GET /v1/apps/{id}/domains/{domain} per-domain status (TLS, DNS, ingress)
DELETE /v1/apps/{id}/domains/{domain} removes row + ingress rule

UI

/apps/{id}/domains (already shipped pre-AD/31) lists every domain with its TLS / DNS badges and an Add / Delete row. AD/31 adds the verification block to the POST response and the POST /verify endpoint; surfacing them in the UI is tracked for a follow-up cycle.

Operator recipe — bump the cap

MAX_DOMAINS_PER_APP is a constant in crates/control-plane/src/routes/domains.rs. Lifting it requires a code change today; cycle 3 will move it to a per-tenant policy in paas_tenant_quotas (same table AC/25 added).

  • Manual Scaling — same paas_tenant_quotas table for resource caps; per-tenant domain caps land there too.
  • Apps — the auto-subdomain (app-{id}.runtime.di2amp.com) is provisioned at app create.