← Control PlaneCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Control Plane
Snapshot Sep 30, 2026 · 23:00 UTC · version 1.0.1
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"name": "domain",
"description": "Custom domains for Control Plane workloads. Use when the user asks to put a domain or subdomain in front of a workload, pick cname vs ns, configure routing or TLS, or hits apex, ownership, or workloadLink errors.",
"included_files": [],
"skill_md_contents": "---\nname: domain\ndescription: \"Custom domains for Control Plane workloads. Use when the user asks to put a domain or subdomain in front of a workload, pick cname vs ns, configure routing or TLS, or hits apex, ownership, or workloadLink errors.\"\n---\n\n# Custom Domains\n\nA `domain` is an org-level resource that binds a DNS name to workloads in **one GVC**. **Created ≠ live:** after the resource exists, the user still adds records at their DNS provider — read exactly which from `status.dnsConfig` and hand them over verbatim, never guessed. Every shape decision below is platform-enforced and a wrong combination is a rejected mutation, so decide BEFORE calling `create_domain` (the tool requires `dnsMode` and `ports` explicitly). Never set `spec.domain` on a GVC — that legacy field is deprecated; the Domain resource is the only path.\n\n## Decide the shape first\n\n**1. Apex or subdomain?** The apex is the registrable root (`example.com`, `example.co.uk`); anything deeper is a subdomain (`app.example.com`).\n\n**2. `dnsMode` — who runs DNS:**\n\n| Mode | Valid for | Wiring | Cert challenge |\n|---|---|---|---|\n| `cname` | **apex (required)** and subdomains | User adds CNAME records per `status.dnsConfig` | `http01` default, `dns01` opt-in |\n| `ns` | **subdomains only** | Delegates the subdomain zone via 4 NS records (`ns1`/`ns2.cpln.cloud`, `ns1`/`ns2.cpln.live`) | `dns01` only — `http01` rejected |\n\n`dnsMode` defaults to `cname` (to `ns` when `gvcLink` is set). The platform rejects `ns` on an apex, and rejects a `cname` domain nested under an existing NS domain (`parent_ns_domain_exists`).\n\n**3. Routing — exactly ONE of three.** All routes in a domain must target workloads in the **same GVC**.\n\n| Mode | What it does | Constraints |\n|---|---|---|\n| `ports[].routes` | Explicit path routes to workloads | The default choice; works for every workload type |\n| `gvcLink` | Every workload in the GVC gets `{workload}.{domain}` | Excludes `workloadLink` and any `ports[].routes`. With `cname` + `http01` it demands `tls.serverCertificate` on every TLS port (http01 cannot issue wildcard certs) |\n| `workloadLink` (spec-level) | Replica-direct: binds the whole domain to ONE stateful workload with per-replica DNS names | **Stateful only** (`workloadLink must link to a stateful workload`); every port exactly ONE route to that same workload; `http01` rejected |\n\nFor an app, site, or API on serverless/standard, the answer is `ports[].routes`. Route-level `workloadLink` inside `routes[]` is a different field with no stateful restriction.\n\n## Ownership and create order\n\n- **`ns` subdomain:** the apex domain resource must already exist in the org (`apex_must_exist`).\n- **Everything else:** ownership is proven either by the org already owning the verified apex (subdomains then attach with no extra records), or by a TXT record — the create fails with `must_prove_ownership` listing the options: `_cpln.{apex}` / `_verify.{apex}`, or `_cpln-{label}.{rest}` / `_verify-{label}.{rest}` at any segment level, value = org GUID **or** org name (TTL 600). The user adds **one**, waits for propagation, and you retry the same create. `create_domain` surfaces these records in its error output.\n- **Apex owned by another org?** The apex name itself is taken (globally unique), but **subdomains still work**: they go through the same TXT proof in this org — the standard multi-org pattern (keep the apex in the production org).\n- **`.internal` domains** are strict same-org — apex and subdomains must live in one org (`apex_owned_by_other_org`, HTTP 409) — and: `cname` only, no `gvcLink`, `certChallengeType` forbidden; every TLS port needs `tls.serverCertificate.secretLink` (no ACME).\n\n## Manifest shape\n\n```yaml\nkind: domain\nname: app.example.com\nspec:\n dnsMode: cname\n ports: # max 10 per domain\n - number: 443 # default 443; 443 + http/http2 auto-gets a TLS block\n protocol: http2 # http | http2 | tcp (tcp needs a dedicated load balancer)\n routes: # max 150 per port (200 with tag cpln/routeLimitOverride)\n - prefix: /api # prefix XOR regex (RE2); prefix defaults to \"/\"\n replacePrefix: / # optional rewrite before forwarding\n workloadLink: //gvc/GVC/workload/API\n port: 8080 # optional target container port\n - prefix: /\n workloadLink: //gvc/GVC/workload/FRONTEND\n```\n\n- **Longest prefix wins** — prefix routes are auto-sorted; regex routes are NOT sorted, written order matters. Duplicate prefix+host combinations are rejected (`There are more than one routes for the prefix …`).\n- **Listener ports other than 443/80 — and the `tcp` protocol — require a dedicated load balancer.** Without one the domain deploys into `warning` (`Unable to configure port …`) instead of serving.\n- **Subdomain matching on one domain** (`hostPrefix` / `hostRegex`, mutually exclusive) requires `acceptAllHosts` or `acceptAllSubdomains` (which exclude each other) AND a GVC with a dedicated load balancer. `hostPrefix` charset: alphanumeric, dot, underscore, hyphen.\n- **Header rewrites** (`headers.request.set`): values may use only `%REQUESTED_SERVER_NAME%`, `%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%`, `%START_TIME%`.\n- **Traffic mirroring** per route: `mirror: [{workloadLink, percent 0-100, port}]` — same GVC, response comes only from the primary.\n- **CORS** per port: `allowOrigins` entries take `exact` XOR `regex`; header lists are lowercased; `maxAge` format is digits + `h`/`m`/`s` only.\n- **TLS** per port: `minProtocolVersion` default `TLSV1_2`; custom cert = keypair secret (PEM) on `serverCertificate.secretLink`; `clientCertificate` enables mTLS verification — client cert details reach the workload in the XFCC header.\n\n## Certificates\n\n- Let's Encrypt, auto-provisioned for port 443 once validation passes; ~90-day certs renewed automatically.\n- `cname` defaults to `http01`: DNS must already resolve and `/.well-known/acme-challenge/` must redirect to the platform solver (`http01-solver.cpln.io`) — a CDN/WAF forcing HTTPS or blocking the path breaks it; switch to `dns01`. `ns` always uses `dns01`. The tag `cpln/skipDNSCheck: \"true\"` skips the DNS-propagation gate in certificate processing.\n- **`dns01` on a `cname` domain adds an extra record**: a `_acme-challenge.{host}` CNAME appears in `status.dnsConfig` — without it the certificate never issues.\n- Wildcard certs come only from `dns01` — that is why `cname` + `gvcLink` + `http01` demands a custom certificate.\n\n## After create — DNS records and status\n\n- Read the domain back and give the user the records from `status.dnsConfig`. CNAME mode points at the GVC endpoint alias. Via the MCP tools the alias is resolved for you, so the CNAME target comes back ready to paste (e.g. `0p2fpmbe7sr5c.t.cpln.app`); via the `cpln` CLI the value is the literal `<gvcAlias>.t.cpln.app` placeholder — substitute the GVC's top-level `alias` field. **Never hand the user a `<gvcAlias>` placeholder as a DNS record.** `cname` + `gvcLink` needs one CNAME per workload; `workloadLink` adds per-replica records (`{workload}-{i}-{location}`).\n- Many DNS providers refuse CNAME at the apex — the user needs ALIAS/ANAME support or a CDN in front.\n- `status.status`: `initializing`, `pendingDnsConfig` (records not seen yet), `pendingCertificate` (validated, cert issuing), `ready`; `warning`/`errored` carry detail in `status.warning`; `usedByGvc` marks a domain referenced by the legacy GVC `spec.domain`. Pending states are expected, not errors. Misconfigurations that pass schema validation — routes to a missing GVC/workload, no valid routes, a disallowed port/protocol, an ignored `hostPrefix` — land as `warning` and increment the `domain_warnings` metric.\n- **Host header:** serverless workloads receive the canonical endpoint as `Host` (the custom domain arrives in `X-Forwarded-Host`); standard/stateful receive the custom domain.\n- Report honestly: \"domain created, routes configured, DNS records pending at your provider\" + the record list. Never claim the domain is serving before DNS exists.\n\n## Platform rejections and exact fixes\n\n| Error | Fix |\n|---|---|\n| `cname is the only valid dnsMode for apex domain X` | Use `dnsMode: cname` on the apex; `ns` only delegates a subdomain zone |\n| `The apex domain X must be created before Y` | Create the apex domain resource first, then the subdomain |\n| `apex_owned_by_other_org` (409) | `.internal` only — internal apex + subdomains stay in one org. A public subdomain under another org's apex just needs the TXT proof |\n| `must_prove_ownership` | Hand the user ONE TXT record from the response, wait for propagation, retry |\n| `parent_ns_domain_exists` | A CNAME domain cannot live under an NS domain — create it as part of the NS zone or restructure |\n| `workloadLink must link to a stateful workload` | Drop spec-level `workloadLink`; route serverless/standard via `ports[].routes` |\n| `Only one of gvcLink or ports.routes may be configured` | Pick one routing mode |\n| `when workloadLink is configured, every port must have exactly ONE route` / `no route can reference another workload` | One route per port, all to the linked workload — or drop `workloadLink` |\n| `certChallengeType can not be http01` / `http01 … not supported for dnsMode ns` | Use `dns01` or omit `certChallengeType` |\n| `Domains may only route to Workloads in a single GVC` | Split into one domain per GVC, or move the workloads |\n| `hostPrefix or hostRegex can only be used if …` | Set `acceptAllHosts` or `acceptAllSubdomains` (and use a dedicated load balancer) |\n| `number of routes exceeds maximum of 150` | Consolidate routes, or add tag `cpln/routeLimitOverride` (raises to 200) |\n\n## Verify\n\n1. `get_resource` (kind `domain`) — `status.status` progressing, `status.dnsConfig` matches what the user added.\n2. After the user adds records: `dig TXT _cpln.DOMAIN` / `dig CNAME DOMAIN` to confirm propagation before retrying or polling.\n3. Once `ready`: `curl -I https://DOMAIN/PATH` and confirm each prefix lands on the intended workload.\n\n## Quick reference — MCP tools\n\n| Tool | Action |\n|---|---|\n| `create_domain` | Create — `dnsMode` and `ports` required; pre-validates apex/exclusivity rules; surfaces ownership TXT records on failure |\n| `update_domain` | Description/tags, `acceptAll*` flags, `gvcLink`/`workloadLink` bind or remove. CANNOT touch ports, dnsMode, certChallengeType |\n| `add_domain_port` / `remove_domain_port` | Add a listener (errors if the number exists) / remove one (destructive — live traffic on that port stops) |\n| `add_domain_route` / `update_domain_route` / `remove_domain_route` | Manage routes on a port; update/remove identify the route by `routeIdentifier` (`prefix` or `regex`); removal 404s matched traffic until re-routed |\n| `set_domain_tls` / `clear_domain_tls` | Overwrite or remove the whole TLS block on a port — on 443 with http/http2 the default TLS block comes back (TLS cannot be disabled there) |\n| `set_domain_cors` / `clear_domain_cors` | Overwrite or remove the whole CORS block on a port |\n| `get_resource` / `list_resources` / `delete_resource` (kind `domain`) | Read / list / delete — names are FQDNs, passed as-is; delete is destructive, confirm first |\n\nCLI fallback (read the `cpln` skill first; CI/CD = `CPLN_TOKEN` + `cpln apply`): `cpln domain create` takes only `--name`/`--description`/`--tag` — spec changes go through `cpln domain edit` or `cpln domain get -o yaml-slim` + `cpln apply`. There is no `cpln domain update`.\n\n## Related skills\n\n| Need | Skill |\n|---|---|\n| Workload ports, exposure, canonical URL | `workload` |\n| Dedicated load balancer (wildcard hosts, tcp ports) | `ipset-load-balancing` |\n| CDN/WAF in front, rate limiting | `cdn-rate-limiting` |\n| Keypair secrets for custom certificates | `setup-secret` |\n\n## Documentation\n\n- [Domain Reference](https://docs.controlplane.com/reference/domain.md)\n- [Configure a Domain Guide](https://docs.controlplane.com/guides/configure-domain.md)\n- [Custom Domain Quickstart](https://docs.controlplane.com/quickstart/quick-start-3-custom-domain.md)\n- [cpln domain CLI](https://docs.controlplane.com/cli-reference/commands/domain.md)\n"
}SHA-256: 24fcea0c710c734ec1129c04e2d35c2b7bfafffdac47d80087089225b471d634