Custom domains

district domain adds and removes an app's custom domains. It also checks whether each domain serves the app. An app holds at most 2 custom domains. Each domain is attached to one durable target of the app, and it serves the deployment that target holds. A domain serves the app after its owner publishes the DNS records the platform names. A check must also find the domain and its certificate active.

Every subcommand takes the repository from the clone in the current directory, or from a trailing positional argument, and accepts --workspace <id> and the environment flags. A repository with no app is a usage error that names district app create. A hostname argument is compared in lower case without a trailing dot, and the API stores it in that form. A hostname the app does not hold is a usage error that lists the domains it holds.

Each subcommand exits 3 when the certificate provider does not answer. The domain is unchanged. Run the command again after the number of seconds in the message.

In a terminal, each status carries a symbol and a color: ✔ active, ○ pending, ✖ failing. A status has one of four values:

  • pending: the platform waits for the records or the certificate.
  • active: the domain serves the app.
  • failing: the domain serves the app, and checks fail.
  • detached: the platform no longer holds the enrollment.

district domain add

district domain add <hostname> [<repository>] [--target <label>] [--workspace <id>] [--json] [--production | --staging | --dev]

Attaches the domain to one durable target of the app and enrolls it. The command prints the target the domain serves, the two DNS records the owner must publish, the optional one, and the command that checks them.

A repeat for a domain the app already holds does not ask for a target. It prints the domain, the records that apply at that time, and the target the domain keeps. A repeat does not move the domain to another target. --target with another label is a usage error that names district domain remove.

--target <label> names the target. The label must be a durable target of the app. A preview target expires, so a custom domain cannot serve one. A label that names no durable target is a usage error. The error lists the durable labels, and the command enrolls nothing.

Arguments

ArgumentDescription
<hostname>The custom domain to attach, such as shop.example.com.
[<repository>]The repository, by repo_ id or by slug. Without it, the clone in the current directory names one: its origin must be a District remote, which also names the environment.

Flags

FlagDescriptionDefault
--target <label>The label of the durable target the domain serves. Without it, a terminal asks, and a session with no terminal sends no label.asks in a terminal; the primary target without one
--workspace <id>The workspace of the repository, when the same slug names a repository in more than one workspace.-
--jsonPrints the domain, with dnsRecords, as one JSON object.false
--production | --staging | --devThe environment to act in. Pass one at most. Without a flag, the command uses the environment that the last district login recorded, or production.-

Output

$ district domain add shop.example.com --target shopfront
added shop.example.com (hdom_01K4…)
status ○ pending
target shopfront

Publish these DNS records, then run: district domain verify shop.example.com
TYPE   HOST                  VALUE                                                    PURPOSE
CNAME  shop                  hosting.ondistrict.net                                   routing
TXT    _district-verify      district-verify=7d3f9c2e-8a41-4b6e-9f0c-1d2e3f4a5b6c     ownership

Optional. Publish it to issue the certificate before you change the routing record:
CNAME  _acme-challenge.shop  a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d.dcv.cloudflare.com  dcv-delegation

Each HOST is relative to example.com. If your provider asks for the full name, add .example.com to it.
The TXT record at the zone apex covers every hostname under it in this workspace.
If you cannot edit the zone apex, publish the same record at the hostname or at any level between.

HOST is relative to the zone, as most provider forms take it: the form appends the zone itself. A form that asks for the full name takes the host with .<zone> after it. The two records above the blank line are required. The routing record sends the domain's traffic to the platform. The ownership record is a TXT at _district-verify on the zone apex. Its value holds the workspace's token, so one record covers every hostname the workspace adds under that zone. An owner who cannot edit the zone apex publishes the same record at the hostname or at any level between. The domain serves after a check finds both records.

The dcv-delegation record is optional. It lets the platform issue the certificate before the routing record changes. Without it, the platform proves control of the domain to the certificate authority over HTTP after the routing record resolves, and renews the certificate the same way.

For an apex domain such as example.com, the routing record's host is @, the form's name for the zone itself. A CNAME on the zone is not valid DNS, so that row reads CNAME* and a footnote names the record types a provider offers instead:

* A root record cannot be a CNAME at most providers. Select ALIAS, ANAME or CNAME flattening for it.

A recently enrolled domain can return no delegation record while the provider's read catches up. district domain status reads the records again.

Without --target, a terminal asks for the target:

Select the target for shop.example.com:
  1) shopfront (shopfront.ondistrict.net) [primary]
  2) next (next.ondistrict.net)
  3) Create a new subdomain
Enter a number from 1 to 3, or press Enter for shopfront:

An empty answer selects the primary target. An app with no primary target has no default. The last entry creates a target: the command asks for its label and offers the first label of the domain. It then attaches the domain to the new target. An empty label takes the offered one. The end of input at the label question creates nothing. If the enrollment then fails, the error names the new target and district target delete. Three answers that select nothing end the command. The error names --target.

A session with no terminal does not send a target. The API attaches the domain to the app's primary target and refuses an app with no primary target. Its message names --target.

The API refuses a request if another app holds the domain, the app is at its cap, or the domain is inside a platform-owned zone. It also refuses a request when the caller cannot update the app. It uses its own message for each refusal.

Exit codes

Exit codeMeaning
0The domain is attached to a target, and the records are printed.
1The API refused the call with its own message: another app holds the domain, the app is at its cap, the domain is inside a platform-owned zone, the app has no primary target, or the caller cannot update the app.
2The command line names no repository, names a repository with no app, passes a --target label that no durable target holds, or passes --target with another label for a domain the app already holds. Three answers that select nothing at the prompt also exit 2.
3The certificate provider did not answer. The domain is unchanged.

district domain list

district domain list [<repository>] [--workspace <id>] [--json] [--production | --staging | --dev]

Prints a header row, then one row per domain: the host, the label of the target it serves, the status, whether it serves the app, the certificate expiry, and when it was added. - stands for a certificate that is not issued. For an app with no custom domain, the command prints nothing and exits 0.

Arguments

ArgumentDescription
[<repository>]The repository, by repo_ id or by slug. Without it, the clone in the current directory names one: its origin must be a District remote, which also names the environment.

Flags

FlagDescriptionDefault
--workspace <id>The workspace of the repository, when the same slug names a repository in more than one workspace.-
--jsonPrints the domains as one JSON array, without dnsRecords.false
--production | --staging | --devThe environment to act in. Pass one at most. Without a flag, the command uses the environment that the last district login recorded, or production.-

Output

$ district domain list
HOSTNAME          TARGET     STATUS     VERIFIED  CERT EXPIRES          ADDED
shop.example.com  shopfront  ✔ active   yes       2026-12-01T00:00:00Z  2026-08-25T09:00:00Z
www.example.com   next       ○ pending  no        -                     2026-08-26T09:00:00Z

Exit codes

Exit codeMeaning
0The rows were printed. An app with no custom domain also exits 0.
1The API refused the call with its own message.
2The command line names no repository, a slug that several workspaces hold, or a repository with no app.
3The certificate provider did not answer. The domain is unchanged.

district domain status

district domain status <hostname> [<repository>] [--workspace <id>] [--json] [--production | --staging | --dev]

Prints the domain's state, the target it serves, the certificate expiry, when it was added, and the DNS records to publish. For an apex domain, the routing row reads CNAME* with @ as its host and the footnote, as under district domain add.

Arguments

ArgumentDescription
<hostname>A custom domain the app holds.
[<repository>]The repository, by repo_ id or by slug. Without it, the clone in the current directory names one: its origin must be a District remote, which also names the environment.

Flags

FlagDescriptionDefault
--workspace <id>The workspace of the repository, when the same slug names a repository in more than one workspace.-
--jsonPrints the domain, with dnsRecords, as one JSON object.false
--production | --staging | --devThe environment to act in. Pass one at most. Without a flag, the command uses the environment that the last district login recorded, or production.-

Output

$ district domain status shop.example.com
hostname      shop.example.com (hdom_01K4…)
target        shopfront
status        ○ pending
verified      no
cert expires  -
added         2026-08-25T09:00:00Z

TYPE   HOST                  VALUE                                                    PURPOSE
CNAME  shop                  hosting.ondistrict.net                                   routing
TXT    _district-verify      district-verify=7d3f9c2e-8a41-4b6e-9f0c-1d2e3f4a5b6c     ownership

Optional. Publish it to issue the certificate before you change the routing record:
CNAME  _acme-challenge.shop  a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d.dcv.cloudflare.com  dcv-delegation

Each HOST is relative to example.com. If your provider asks for the full name, add .example.com to it.
The TXT record at the zone apex covers every hostname under it in this workspace.
If you cannot edit the zone apex, publish the same record at the hostname or at any level between.

Exit codes

Exit codeMeaning
0The domain was printed. A domain that is not active is not a failure.
1The API refused the call with its own message.
2The command line names no repository, no domain of the app, or a repository with no app.
3The certificate provider did not answer. The domain is unchanged.

district domain verify

district domain verify <hostname> [<repository>] [--workspace <id>] [--json] [--production | --staging | --dev]

Checks the domain now instead of waiting for the platform's own schedule. The command then prints the domain after the check, as district domain status prints it without the records. The command exits 0 only when the status is active.

One check of a domain runs at a time. For two minutes after a check starts, another call does not start a second check. It prints the domain as it stands.

Arguments

ArgumentDescription
<hostname>A custom domain the app holds.
[<repository>]The repository, by repo_ id or by slug. Without it, the clone in the current directory names one: its origin must be a District remote, which also names the environment.

Flags

FlagDescriptionDefault
--workspace <id>The workspace of the repository, when the same slug names a repository in more than one workspace.-
--jsonPrints the domain as one JSON object, without the note. It uses the command's exit code.false
--production | --staging | --devThe environment to act in. Pass one at most. Without a flag, the command uses the environment that the last district login recorded, or production.-

Output

$ district domain verify shop.example.com
hostname      shop.example.com (hdom_01K4…)
target        shopfront
status        ✔ active
verified      yes
cert expires  2026-12-01T00:00:00Z
added         2026-08-25T09:00:00Z

While the domain is not active, the command prints a note above the verdict: published records take minutes to propagate, the platform's own checks run while the domain is pending, and district domain status prints the records.

A shell loop can wait for the domain:

until district domain verify shop.example.com; do sleep 30; done

Exit codes

Exit codeMeaning
0The check found the domain active.
1The check found the domain not active, or the API refused the call with its own message.
2The command line names no repository, no domain of the app, or a repository with no app.
3The certificate provider did not answer. The domain is unchanged.

district domain remove

district domain remove <hostname> [<repository>] [--yes] [--workspace <id>] [--production | --staging | --dev]

Removes the domain from the app. The domain then stops serving the app. The owner's DNS records are not changed. Remove them separately.

The command prints the resolved domain and the environment. It then asks Remove <hostname>? [y/N].

Arguments

ArgumentDescription
<hostname>A custom domain the app holds.
[<repository>]The repository, by repo_ id or by slug. Without it, the clone in the current directory names one: its origin must be a District remote, which also names the environment.

Flags

FlagDescriptionDefault
--yesSkips the "Remove <hostname>? [y/N]" question. A session with no terminal needs it.false
--workspace <id>The workspace of the repository, when the same slug names a repository in more than one workspace.-
--production | --staging | --devThe environment to act in. Pass one at most. Without a flag, the command uses the environment that the last district login recorded, or production.-

Exit codes

Exit codeMeaning
0The app no longer holds the domain.
1The confirmation was declined, or the API refused the call with its own message.
2The command line names no repository, no domain of the app, a repository with no app, or no terminal can confirm.
3The certificate provider did not answer. The domain is unchanged.