Notes

The hostname is composed, not asked for

deploy_site no longer asks for a hostname. juudd composes it from the site id, refuses a value that disagrees, and reports where the site really answers.

When your assistant calls deploy_site, it does not choose the hostname. juudd composes it from the site id and the domain it serves sites under, so a site called shop answers at shop.juudd.app. If the assistant passes a hostname that disagrees, the deploy is refused and the sentence says what the real name is.

That is the decision. The rest of this post is the deploy that led to it, the three things that changed, and what we left alone.

A working site, reported at an address that did not exist

deploy_site used to take subdomain as a required argument, described as the hostname the site is served at, and it stored whatever arrived. One deploy passed a bare label with no domain on it. The reply printed that label as the URL. A link with no domain resolves nowhere, and the owner reported a broken deploy.

The deploy had not failed. We measured both names before changing anything:

AddressWhat it did
the composed name, <site>.juudd.appanswered 200 with the site's own page
the bare label the reply printedDNS failure, no such host

The site was on the internet the whole time, at the name the platform would have composed. It was reported at a name nobody could reach.

Nothing ever read the value

A request is routed by the hostname it arrives on. The router takes one label under the serving domain, turns it into a site id and looks that site up. It never consults the stored bundle.

So subdomain was write-only: stored at deploy time, read back only to print a URL, and free to disagree with reality without anything noticing. The site worked and the report lied, which is worse than either half alone, because the owner's next move is to debug a working system.

There is also exactly one hostname that routes to a given site, which is the site id under the serving domain. The caller, a model reading a schema, was being asked for a value where every answer but one was wrong and being wrong was invisible. That is not a validation gap. The argument should not have existed.

Four changes

  • The hostname is composed by one function, hostnameFor(site, siteDomain), that lives beside the function doing the inverse. The two directions of the mapping have to agree, and a name composed by hand anywhere else is how you get one the router would not recognise.
  • subdomain is optional, and its description tells the assistant to omit it and says why: a hostname passed there has never been what put the site on the internet, only what the reply printed.
  • A value that disagrees is refused, not corrected. The refusal names the composed hostname and points at claim_domain, the tool that adds a second hostname when you own a domain.
  • What a publish reports is composed too, and so are the lines in read_site and list_deploys that print an address. A rollback republishes a stored version, and without this it would have repeated the bad address in the middle of a recovery.

Why a refusal and not a quiet correction

A hostname like shop.example.com for the site shop is not a typo to repair. It is the caller telling us where it believes the site answers. Storing something else would leave that belief intact, and the next reply would be confused by it. So the deploy stops, the refusal is recorded like every other refusal, and the sentence says how to make the belief true: deploy, then claim_domain.

What we left alone

Nothing stored was rewritten. The version that started this still records the bare label. It is an accurate record of what was said at the time, every reader now composes the name instead, and redeploying a live site to correct a metadata string would risk the site to fix the description of it.

A site with its own domain still reports the platform name. The composed name never stops working, so it is not wrong, but it is not the whole truth for a site that also answers on a domain of its own. Where that lives is the custom domain records, not this change.

Where there is nothing to compose from, the caller is still asked. A deployment with no serving domain it knows about has no name to compose, so there a hostname passed in is stored as given.

The rule, once

Do not ask the caller for a value only one answer can satisfy: juudd composes the hostname from the site id, refuses one that disagrees, and reports the name that answers. The tools and their limits are listed in the docs.