# Connect a custom domain to Rogue

Customers keep their domain registrar and DNS provider. They need a registered
domain, access to its DNS settings, a public Rogue publication they own, and Pro
access (paid or a complimentary platform-admin grant). They do not need their own
Cloudflare subscription. Pro also removes the Rogue top banner from their websites
and apps automatically, including complimentary grants. Rogue registers the hostname with Cloudflare and manages
HTTPS; the customer adds the DNS records. Rogue's MCP/API does not edit their DNS.

### Connect a hostname

The native **rog 0.5.0+** CLI provides the same account-scoped workflow. Use
`rog --help` for global options and `rog domains setup` for DNS instructions.
Set `ROGUE_API_KEY` privately, or put `--key-file /private/rogue.key` before the
command. Global options always precede the command.

```sh
rog domains capabilities
rog call list_publications
rog domains create www.example.com PUBLICATION_UUID
rog domains list
rog domains dns CLAIM_UUID
# After the customer saves the returned records and the cooldown has elapsed:
rog domains check CLAIM_UUID
rog domains status CLAIM_UUID
```

`status` and `dns` read the saved claim, including every diagnostic field; `check`
(alias `refresh`) performs one fresh DNS/provider check. Commands return JSON,
preserve `next_steps`/tracking/errors and never edit customer DNS. There is no
automatic polling or write retry. Exit 0 means the request succeeded, not that
the domain is live; inspect the state and HTTPS output. `rog domains remove
CLAIM_UUID` explicitly disconnects a claim. On older CLI releases, use `rog call`
with the MCP tool names below, or install the current release again.

1. Call `bootstrap` or `publishing_capabilities` with the customer's existing
   Rogue credential. Follow `publishing.custom_domains.next_steps` in bootstrap
   (`custom_domains.next_steps` in publishing capabilities): Pro access, available
   slots, `agent:write` and platform hosting configuration are separate checks.
   There are five hostname slots per Pro account. After an admin grant, refresh
   MCP `tools/list` and `bootstrap` with the same key. Cached MCP clients may need
   a connection refresh; Rogue does not push tool-list change notifications.
2. Choose the exact hostname, such as `www.example.com` or `app.example.com`, and
   the owned public publication's UUID (use `list_publications` to find it).
   Call `create_custom_domain({"hostname":"www.example.com","publication_id":"<publication UUID>"})`,
   or `POST /api/v1/me/domains` with the same JSON. Use a hostname only: no
   `https://`, path, port or wildcard. Save the returned claim `id`; it is distinct
   from `publication_id`. Retry an uncertain create with the same two inputs.
3. Show the customer a DNS table from **every returned `dns_records` entry**:
   `type`, `name`, `value` and `purpose`. Copy the actual returned values, never
   example tokens or a guessed Rogue IP/target. At the DNS provider, `name` maps
   to **Name/Host**, and `value` maps to **Value/Target/Content**. TTL Auto/default
   is suitable. Save each record and keep unrelated mail/verification records.
4. Call `refresh_custom_domain({"id":"<claim UUID>"})`, or
   `POST /api/v1/me/domains/{id}/refresh`, after DNS changes and the cooldown.
   Wait at least `refresh_after_seconds` (currently 60) between provider checks;
   honor `retry_after_seconds` and HTTP `Retry-After` if present. Re-read all
   records: certificate-validation records may appear on a later refresh. Add
   any newly returned records and repeat at the permitted cadence. DNS and TLS
   can take longer than the cooldown; a successful API call does not mean ready.
   If `next_steps` reports `retry_hostname_creation`, creation never reached the
   provider. Refresh the same claim after the cooldown to resume it; keep its ID
   and DNS proof. Pro and publication access are rechecked. An uncertain submitted
   create is only reconciled, never blindly repeated. Follow `contact_admin` when
   returned, and include the claim ID and `error` in the support report.
5. Only report validation complete when `state` is `active`,
   `ownership_verified` is true, and **both** `hostname_status` and `tls_status`
   are `active`. Open the returned HTTPS `url` and check the intended content
   before reporting the site live. The publication must remain public, the owner
   active with Pro, and platform hosting available.

### DNS fields and apex domains

For `www.example.com`, when editing the DNS zone `example.com`:

| Purpose | Type | Returned full Name | Name if the provider appends `.example.com` | Value |
| --- | --- | --- | --- | --- |
| Route visitors | CNAME | `www.example.com` | `www` | Returned routing `value` |
| Prove ownership to Rogue | TXT | `_rogue-challenge.www.example.com` | `_rogue-challenge.www` | Returned ownership `value` |
| Cloudflare ownership or HTTPS | TXT or CNAME | Exact returned `name` | Remove only the provider's automatically appended zone suffix | Exact returned `value` |

Some providers accept full names; others append the zone automatically. Check the
saved name so it does not become `www.example.com.example.com`. Keep multiple TXT
values if returned at the same name. If a CNAME conflicts with an existing A,
AAAA or CNAME at the chosen hostname, review the existing site's routing before
replacing it. DNS cutover can interrupt the old site while validation is pending.

At an external DNS provider, prefer a **CNAME on `www`** pointing to the
returned routing target. A bare/apex domain needs a supported CNAME setup;
Cloudflare DNS supports apex CNAME flattening. A third-party ALIAS/ANAME that
only returns shared Cloudflare IPs does not establish the SaaS CNAME relationship.
An active certificate or matching A/AAAA addresses alone does not prove routing.
Do not instruct customers to copy Rogue's resolved IPs into A/AAAA records.
Rogue does not currently provision Enterprise apex proxying or dedicated apex IPs.

If the customer hosts DNS on Cloudflare, create a CNAME to the returned SaaS target
at the exact hostname (`@` for the apex). A proxied CNAME uses Cloudflare's
supported O2O routing. Hostname activation still requires this CNAME relationship;
TXT/HTTP ownership tokens cannot replace it. Preserve routing records after setup.

**Gandi LiveDNS example:** keep DNS at Gandi and use **CNAME**, Name **www**,
the returned routing target with a final dot (currently `customers.rogue.camp.`),
and TTL **300 seconds**. Add the exact proof/TLS records returned for the separate
`www` claim; do not reuse the apex claim's proof values. Gandi rejects CNAME at `@`.
Earlier guidance suggesting `@ ALIAS` as an equivalent SaaS setup was incorrect:
Gandi may accept it, but Cloudflare can reject the resulting hostname routing.

For the simplest Gandi recovery, create a separate `www` claim for the same
publication, use its new DNS proofs, and verify `https://www.example.com` works.
Then configure **Gandi Web Forwarding** from the bare domain to that HTTPS URL.
Choose a normal permanent redirect when that is the intended permanent address,
enable forwarding for **HTTP and HTTPS**, and complete any certificate activation
Gandi requests. Test both schemes; an HTTP redirect alone does not repair HTTPS.
Adjust conflicting apex records as Gandi's forwarding setup requires while
preserving mail and unrelated records. The redirect is served by Gandi, so the
bare domain does not need a Rogue claim for forwarding alone. Once forwarding
works, an unused apex claim can be removed to release its slot; do not remove the
working `www` claim. See [Gandi's forwarding guide](https://docs.gandi.net/en/domain_names/common_operations/web_forwarding.html).

To retain the bare domain, the customer can move DNS hosting to Cloudflare while
keeping domain registration at Gandi. Copy the complete zone, including mail,
SPF/DKIM, other TXT and SRV records, then coordinate the nameserver/DNSSEC change
and add the apex CNAME to the SaaS target. Do not change nameservers, disable
DNSSEC or remove unrelated records without the customer's approval. Alternatively,
a separately provisioned apex-proxying service is required; do not promise it from
Rogue's standard SaaS subscription.

The apex and `www` are separate hostnames and consume separate slots. Claim both
if both should serve the publication. Rogue does not automatically redirect one
to the other; a redirect needs separate configuration at the customer's provider
or application, including HTTPS support on the hostname receiving the redirect.
Wildcard claims and Rogue-owned platform hostnames are unavailable.

### Check, troubleshoot and disconnect

`list_custom_domains` (`GET /api/v1/me/domains`) and `get_custom_domain`
(`GET /api/v1/me/domains/{id}`) read stored status without contacting Cloudflare.
Use `refresh_custom_domain` to fetch current validation results. Domain responses
include `dns_note`, `next_steps`, `error_code`, `error` and the latest known DNS requirements.

Keep the stable claim `id` and authenticated `status_url` to track the workflow.
Read `operation_in_progress`, `checked_at` and `retry_after_seconds`; a successful
request is not the same as an active domain. Cooldown errors carry `rate_limited`,
`claim_id`, `status_url` and `retry_after_seconds` (REST also sends `Retry-After`).
Do not recreate the claim just to check progress. There are no push notifications:
use explicit refresh calls, then inspect the full returned snapshot. If using
`Idempotency-Key` or MCP `_idempotency_key`, retain it for retries of one uncertain
refresh; use a new key for the next distinct check after that operation completes,
otherwise its saved response is replayed.

Each `dns_records` entry includes `check.status`, `check.message`, `checked_at`,
`observed_type`, `observed_values`, `ttl_seconds` and `resolver`:

- `verified` (green): the expected value is visible to the resolver.
- `pending` (yellow): missing or not checked yet. Add if absent; otherwise wait.
- `incorrect` (red): another value is visible. Compare expected and observed
  values; after a recent correction, the previous TTL may still be caching it.
- `unknown` (yellow): resolver unavailable or proxy/flattening inconclusive.
  Do not tell the customer to replace a record based only on this result.

`dns_summary` counts these results. This is a snapshot from Cloudflare DNS,
not a guarantee of worldwide propagation. Matching flattened addresses remain
unconfirmed until Cloudflare also activates the hostname; address equality alone
is insufficient. Different addresses can also reflect a proxy or geographic DNS.

**Instructions change during validation and renewal.** Compare `dns_revision`
with the previous response: it changes when required names/values change, not
when a check timestamp changes. Show newly returned records and updated values.
`previous_dns_records` keeps up to 32 recently withdrawn values with
`no_longer_requested_at`, starting when tracking was introduced. It is not a
complete audit log. An ACME challenge leaving the current list can accompany
certificate issuance; use `tls_status` to confirm. Its disappearance alone is
not an instruction to delete it or proof that the domain is active.

Stored operation errors have stable `error_code` values:
`provider_routing_required`, `provider_hostname_validation_pending`, `provider_tls_validation_failed`,
`provider_request_failed`, `provider_hostname_conflict`,
`provider_hostname_missing`, `provider_validation_failed` or
`domain_operation_unconfirmed`. These may accompany a successful HTTP response;
read `error` and `next_steps`, and give support the claim ID and code. Hostname
and certificate validation problems are mapped to safe guidance, including
unconfirmed routing and CAA restrictions. `provider_routing_required` with
`change_dns_routing` means the DNS setup must change; waiting or recreating the
claim will not repair an unsupported ALIAS/proxy arrangement. DNS
mismatches are reported separately in each record's check, even when `error` is null.
If validation stays pending, check the saved DNS names/values at the authoritative
provider, newly returned TLS records, CNAME conflicts and restrictive CAA records.
Follow the returned error; ask Rogue support about provider/hostname conflicts.
Do not repeatedly create claims or promise a fixed activation time.

Keep routing and verification records in place while using the domain. Cloudflare
attempts automatic renewal for active, exact hostnames; an inactive hostname may
need fresh validation records. If validation needs attention, refresh and follow
the returned records rather than reusing an old token.

Use `remove_custom_domain` (`DELETE /api/v1/me/domains/{id}`) to disconnect. Routing
stops immediately; retry the same ID while `state` is `deleting`, until `removed`.
Then remove or repoint the customer's corresponding DNS records. Pending, failed
and deleting claims consume slots until removal is confirmed. Removal remains
available after Pro expiry. Revoked/expired Pro stops custom-domain serving but
preserves the publication and its Rogue URL, subject to its normal visibility.
Reads require `agent:read`; mutations require `agent:write`, via a bearer header.
The publication's normal sandbox and GET/HEAD hosting rules still apply; its app
MCP/API endpoints remain at the normal Rogue URLs.

This walkthrough is also available at
[Rogue's custom-domain guide](https://rogue.camp/guides/custom-domains.md).
Provider details: [Cloudflare hostname setup](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/domain-support/create-custom-hostnames/),
[apex DNS](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/start/advanced-settings/apex-proxying/),
[validation CNAMEs](https://developers.cloudflare.com/dns/manage-dns-records/troubleshooting/cname-domain-verification/),
[proxied customer DNS](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/saas-customers/how-it-works/),
and [certificate renewal](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/issue-and-validate/renew-certificates/).
