Attach your own domain #

View this page as Markdown

Prompt:

Put our app on app.example.com.

Also works:

  • "Serve the app from our own domain instead of the ai.host address."
  • "Why does app.example.com still say pending?"

What your tool does #

  • Reads the custom-domain skill (read_context, id skill:custom-domain) and follows it.
  • Checks that the application is on Pro, the plan that supports a domain of your own, and asks you before it calls set_plan.
  • Asks you for the exact hostname, such as app.example.com, and tells you how many of your users' passkeys the attach takes out of use before you write any record.
  • Adds the custom_domain entry to the manifest's services and submits the whole manifest with submit_manifest, which needs no browser approval.
  • Hands you the CNAME record from the answer and the TXT record from read_status, exactly as the platform gives them.
  • Writes nothing at your DNS provider itself, unless you have connected the provider to your tool and ask it to.
  • Waits with read_status until the domain's state reads serving, then opens the domain to confirm it answers.
  • Before it removes the domain, tells you that the detach takes your users' passkeys on the domain out of use, and asks you first.

What you need #

Before your AI starts #

This section is for your AI tool: what it checks and gathers before it begins. You don't need to do these steps yourself.

  • A connected, signed-in tool (Connect Claude Code or Codex).
  • The application's identifier from list_applications, and its plan from read_usage, which must read pro.
  • The application's current manifest, because a submission replaces the whole document.

Steps #

1. Choose the name #

One application attaches one hostname, on its production environment only. The development hostname stays as it is.

The hostname is lowercase and has at least three labels, such as app.example.com. A bare domain such as example.com or example.co.uk cannot be attached: attach www or another name under it instead. A wildcard, a public suffix, and a name another application already holds cannot be attached either. Neither can a name the platform keeps for itself: any name under ai.host, the edge name your CNAME record points at among them, and any name under turnzero.ai. A name that is not lowercase, has fewer than three labels, or is a wildcard is refused manifest_invalid.

2. Declare the domain in the manifest #

Your tool adds this entry to services and submits the whole manifest:

{ "kind": "custom_domain", "domain": "app.example.com" }

The answer has a custom_domain member with two things. The first is the CNAME record to write at your DNS provider, from your hostname to the platform's edge name, such as edge.ai.host. The second is passkeys_to_strand, how many of your users' passkeys the attach will take out of use (What changes for your users).

The platform then sets the domain up in the background. Nothing changes for the application's visitors until the domain serves (step 5).

A submission on Free or Standard is refused 409 custom_domain_not_on_plan. Any other name step 1 rules out, such as example.co.uk or a name another application holds, is refused 400 custom_domain_invalid, with the reason. Either refusal leaves the manifest as it was.

3. Write the CNAME record #

Your tool hands you the record exactly as answered: its name, the type CNAME, and its value. Write it at your DNS provider, in the zone that holds your domain. The platform writes no record in your zone and never asks for your provider's login.

Until step 5 reads serving, keep every link and address your application hands out on the managed hostname.

4. Write the TXT record #

Within a few minutes, the platform's hosting provider issues the TXT record that proves you control the name, and the platform reads it. You write that record in your own zone, as you wrote the CNAME record; the platform writes nothing there. read_status then shows both records, the passkey count, and the domain's state under the same custom_domain member. Your tool reads the TXT record there, never composing one, and hands you its name, the type TXT, and its value.

If that name already holds a TXT value at your provider, replace it with the new value or add the new value beside it.

While the platform is busy with other domains, the domain's reason reads queued and shows no TXT record yet. If the TXT record still does not appear a few minutes after queued clears, the problem is the platform's, not yours: report it with submit_feedback (Feedback and ratings).

5. Wait until the domain serves #

Your tool re-reads read_status about once a minute. The domain's state moves through these:

State What it means
records_pending The platform is waiting for your CNAME record to be visible on the public internet.
validating The CNAME record was found, and the domain is being checked.
issuing The certificate is being issued, and the domain is being added to the platform's edge.
serving The domain answers your production environment.
detaching The domain is being removed, after you removed the entry (step 7).
failed Something stopped the attach, and the domain's reason says what.

DNS changes take time to spread, so records_pending can last from minutes to an hour or more, depending on your provider.

A failed state shows one of these reasons:

Reason Cause What to do
records_missing The records were not found in time, so the platform asks for a new TXT record value. Check the CNAME record and submit the manifest again. Once the domain is back in records_pending, write the TXT record read_status shows a few minutes later, as in step 4: its value changes.
caa_blocks_issuer A CAA record in your zone does not allow the certificate's issuer. Change or remove that CAA record, then submit the manifest again.
capacity The platform's hosting provider is at its limit for domains. Nothing on your side; ask the platform operator, then submit the manifest again.
provider_refused The platform's hosting provider refused the domain, for the reason in the text. Correct what the text names, then submit the manifest again.
certificate_lapsed A domain that was serving lost its certificate, seen on two hourly checks. Check that both records still stand, then submit the manifest again. Until the domain serves again, your users' sign-in stays on it; detaching the domain (step 7) returns sign-in to the managed hostname.

A failed domain leaves failed only when you submit the manifest again. If the domain never served, nothing changed for your users. If it had served, sign-in stays on the domain while it is failed, until you detach it.

Once the domain serves, two more reasons can appear beside serving. chain_not_found means the CNAME record no longer points at the edge name, though the domain still serves for now: restore the record. revalidation_pending means the certificate's renewal needs a new TXT record, which read_status now shows: write it as in step 4.

6. Confirm #

Open https://app.example.com. The page answers there, and the application's cookies are set on that hostname. The managed production hostname keeps serving the application, but a sign-in started there does not complete (What changes for your users). The development hostname is unchanged.

From now on, hand out links on your domain. Once the domain serves, the platform restarts the production copy, so its APP_PUBLIC_HOST setting reads your domain (The settings a deployed copy receives); build links from that setting. If that restart is refused, read_status shows the refusal as the domain's reason. The copy then keeps the managed hostname until the next deploy, promote, or restart_application.

7. Change or remove the domain #

To detach the domain, remove the entry from services and submit the manifest again. The domain moves to detaching and then stops answering. If the domain had served, the platform restarts the production copy, so APP_PUBLIC_HOST reads the managed hostname again.

To change the name, detach first and wait until read_status no longer shows the custom_domain member. Then declare the new name. A new name declared while the old one is still detaching is refused custom_domain_invalid with the reason held.

While a domain is attached, set_plan onto Free or Standard is refused 409 plan_custom_domain_conflict. Detach the domain and wait until read_status no longer shows it before you move the plan.

What changes for your users #

Your users' passkeys work only on the hostname they were registered on. When the domain first serves, the production sign-in moves to the domain, so every passkey registered on the managed hostname stops working. The number is passkeys_to_strand in step 2's answer, which read_status keeps current until the domain serves. So attach the domain before your users register passkeys, if you can.

Once the domain serves, your users sign in on the domain. A sign-in started on the managed hostname does not complete, whatever its method, and the person signs in again on the domain. Through records_pending, validating, and issuing, nothing changes for your users.

A user whose passkey stopped working signs in with an emailed code or another method on the domain and is offered a new passkey there. Detaching the domain reverses the move: the old passkeys work again, and the ones registered on the domain stop working. The detaching submission's passkeys_stranded gives that count. A rename while the domain is attached retires the old passkeys for good (Rename the application).

A native app that signs in through the application must name the domain. On iOS, the app's associated domains add webcredentials:app.example.com, and the platform serves the association files on the domain as on the managed hostname. An Android app's declared signing fingerprints stay accepted on the domain (Sign in from a native app).

A client whose redirect address is an https address on the domain, such as https://app.example.com/callback, can be declared with configure_realm once you have attached the domain, before it serves. Sign-in accepts that redirect only while the domain carries sign-in, from its first serving until you detach it.

Expected result #

read_status shows the production environment's custom_domain member with the state serving, both records, and passkeys_stranded, the passkeys the attach took out of use. https://app.example.com answers the application, and the managed production hostname still serves its pages, with sign-in on the domain. After the platform's restart, APP_PUBLIC_HOST in the production copy reads your domain. The development hostname serves as before.

Refusals #

This table lists the refusals these steps most often meet; the refusals page lists every refusal with its cause and remedy.

Refusal Status Cause Remedy
custom_domain_not_on_plan 409 The manifest declares a domain, and the application's plan does not support one. The plan is named, and nothing is written. Move the application to Pro with set_plan, then submit the manifest again. Or remove the custom_domain entry.
custom_domain_invalid 400 The hostname is refused for the reason given: apex, reserved, public_suffix, or held. Declare a different hostname and submit again: for an apex, a name under it such as www; for a held name, a name of your own. If your own earlier domain holds the name, detach it and wait until read_status no longer shows it first.
custom_domain_capacity 409 The platform's domains stand at their limit, given in bound. Nothing is written. Nothing on your side raises the limit. Ask the platform operator, then submit again. Removing the entry lets the submission through without the domain.
custom_domains_not_yet_served 400 The platform does not serve domains of your own yet. Remove the custom_domain entry and submit the manifest again. The platform operator turns the feature on.
plan_custom_domain_conflict 409 set_plan names a plan that does not support a domain while one is attached. The plan and the domain are named. Remove the entry, submit the manifest, and wait until read_status no longer shows custom_domain. Then repeat set_plan, or choose Pro.
plan_quantity_unset 409 The plan has no value set for whether it supports a domain. The plan and measure members name it, and nothing is written. Choose another plan, or wait for platform staff to set it.