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:
Then call:
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://runtime.di2amp.com/api/v1/apps/$APP/domains/www.example.com/verify
Response:
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:
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).
Related¶
- Manual Scaling — same
paas_tenant_quotastable for resource caps; per-tenant domain caps land there too. - Apps — the auto-subdomain (
app-{id}.runtime.di2amp.com) is provisioned at app create.