Manage

Domain

Claim, verify, and remove the custom domain a workspace serves its exposed environment feeds on.

Usage

bash
envless domain claim <hostname> [--workspace <w>]
envless domain status [--json] [--workspace <w>]
envless domain verify [--reattempt] [--workspace <w>]
envless domain remove [--yes] [--workspace <w>]

A custom domain belongs to a workspace, not to a product or a project, so envless domain sits next to envless member and envless role rather than next to envless var. Claiming a domain proves you own the hostname and provisions a TLS certificate for it.

Once the domain reaches active, it serves your exposed environment feeds directly. The workspace is read from the hostname, so its slug drops out of the path:

bash
https://api.envless.cloud/exposed/acme/watch/website/production/versions/latest   # default
https://vars.acme.com/exposed/watch/website/production/versions/latest            # your domain

Both keep working at the same time. The default URL never goes away, and the long form stays valid on your own domain as long as the workspace slug matches, so a URL you handed out before claiming the domain does not break. Removing the domain drops the branded URL and leaves the default one serving.

A custom hostname is there to serve the exposed feed. /exposed/* is the only surface meant to answer on it, and the dashboard API and the API-key surface stay where they are, on api.envless.cloud.

The envless command ships in @goenvless/cli. Install it with npm i -g @goenvless/cli@latest.

A workspace holds one domain at a time, which is why only claim takes a hostname. Nothing here reads, writes or decrypts a variable, so no passphrase, no ENVLESS_KEY and no unlocked workspace are involved.

Running envless domain with no subcommand, or with nothing but --help, prints the usage block and exits 0.

bash
$ envless domain
envless domain
  Claim, verify, and remove the custom domain that serves your exposed environments.

USAGE
  envless domain claim <hostname> [--workspace <w>]
  envless domain status [--json] [--workspace <w>]
  envless domain verify [--reattempt] [--workspace <w>]
  envless domain remove [--yes] [--workspace <w>]

SUBCOMMANDS
  claim    Point a hostname at this workspace and print the DNS records to add
  status   Show the hostname, where it is up to, and the records it still wants
  verify   Recheck the DNS records and turn the domain on once they are right
  remove   Drop the domain, exposed environments go back to the default URL

OPTIONS
  --workspace <w>  Workspace slug or id, defaults to the .envless file
  --reattempt      Ask for a fresh certificate check, for a domain stuck on SSL
  --json           Print the status payload as JSON and nothing else
  --yes, -y        Skip the remove confirmation

EXAMPLES
  envless domain claim vars.acme.com   Claim the hostname and print the DNS records
  envless domain status                See the hostname and where it is up to
  envless domain verify                Recheck DNS once the records are live
  envless domain verify --reattempt    Try again for a domain stuck on a certificate
  envless domain remove --yes          Drop the domain without a prompt

  A workspace holds one custom domain, so only claim takes a hostname.
  Custom domains need a paid plan and workspace update permission.
  Aliases: rm, del or delete for remove.

Subcommands

SubcommandAlso acceptedTakes
claimno aliasExactly one hostname
statusno aliasNothing
verifyno aliasNothing
removerm, del, deleteNothing

No subcommand takes a list, because a workspace holds a single domain. claim reads the first word after the subcommand and ignores anything after it, so envless domain claim vars.acme.com other.acme.com claims vars.acme.com only. status, verify and remove read no positional at all, and they always act on the one domain the workspace already has.

remove is the only subcommand that confirms. It loads the domain first, so the prompt names the real hostname and a workspace with no domain aborts before any prompt and before anything is deleted. There is one confirmation for the whole run.

Anything else prints ✗ Unknown subcommand: <word>, then the usage block, and exits 2.

Claim

bash
envless domain claim vars.acme.com
envless domain claim VARS.ACME.COM
envless domain claim vars.acme.com --workspace acme

The hostname is trimmed and lower cased before anything else happens, so VARS.acme.com and vars.acme.com are the same claim. Claiming returns the DNS records to add, and the CLI prints them as a TYPE / NAME / VALUE table under the spinner line, with the note the API put on a record printed under the table.

bash
$ envless domain claim VARS.acme.com
┌  envless domain claim
✔ vars.acme.com (pending_challenge)

TYPE  NAME                              VALUE
TXT   _envless-challenge.vars.acme.com  envless-domain-verification=4a7d1f2c9b0e83d5461a7c2ef8b30d95a6c14f7be2093d8c5f1a6b47e9c0d2f3

TXT   Proves this workspace controls the domain.

│
└  ✓ vars.acme.com is claimed. Add the record above at your DNS provider, then run envless domain verify.

A fresh claim asks for one record, the _envless-challenge TXT that proves you control the domain. The CNAME and the certificate records only appear once that TXT has been seen, which is what verify checks first. Add the record, then run verify. A successful claim exits 0.

Add the records with proxying off. On Cloudflare that is the grey cloud, not the orange one. Proxy status does not apply to a TXT record, and the TXT is what the check reads, so a proxied CNAME does not fail verification. It sends the request to your own Cloudflare zone instead, which is how a domain ends up reading active while the feed never loads. The CLI prints the same reminder under the records whenever one of them is a CNAME.

Give that record time to propagate before you verify. Every resolver the API checks has to return it, so a verify run too early leaves the domain on ownership_failed rather than on pending_challenge. Nothing is lost, the next verify moves it on to pending_dns once the record resolves everywhere.

The hostname is checked on your machine before any request goes out. Each of these prints its own message and exits 2, in this order:

bash
$ envless domain claim https://vars.acme.com/path
✗ Drop the scheme and any path. Pass the hostname on its own, like vars.acme.com.
$ envless domain claim vars.acme.com:8080
✗ Drop the scheme and the port. Pass the hostname on its own, like vars.acme.com.
$ envless domain claim benjamin@acme.com
✗ That reads like an email address. Pass a hostname, like vars.acme.com.
$ envless domain claim "vars acme com"
✗ A hostname cannot hold a space. Pass one hostname, like vars.acme.com.
$ envless domain claim localhost
✗ A hostname needs a dot in it, like vars.acme.com.

With no hostname at all, claim prints ✗ Missing the hostname. Run envless domain claim vars.acme.com. and exits 2. There is no prompt, on a TTY or otherwise.

That local check is deliberately shallow. Everything else about the hostname is the API's call, and the CLI prints whatever came back on a failed spinner, then Aborted., and exits 1:

bash
$ envless domain claim '*.acme.com'
┌  envless domain claim
✖ Wildcard domains are not supported.
│
└  Aborted.

The same path carries Enter a domain, not an IP address., Enter a full domain, like env.acme.com., Domain parts may only use letters, numbers and hyphens., Domain must be at most 253 characters., That domain suffix cannot be used., That domain cannot be used. and That domain is already in use by Envless.

Plan gating is never pre-judged. On a workspace whose plan has no custom domain, the claim spinner fails with the API message Custom domains need a paid plan., prints Aborted. and exits 1. A deployment with no Cloudflare configuration answers Custom domains are not configured on this deployment. the same way.

A hostname another workspace has already verified is refused with Another workspace has verified this domain. Claiming a different hostname while one is already claimed replaces it and resets the flow to pending_challenge, with a new challenge record. There is no cap on how many times you may claim.

Claiming the hostname that is already active changes nothing and returns the live record set. The outro still reads is claimed and still points at envless domain verify, and it exits 0.

Status

bash
envless domain status
envless domain status --json
envless domain status --workspace acme

Plain output, with no prompt box and no spinner, so it pipes cleanly into grep, head, or anything else. Three aligned fields, then the records the domain still wants.

bash
$ envless domain status
HOSTNAME  vars.acme.com
STATUS    pending_dns
VERIFIED  no

TYPE   NAME                              VALUE
CNAME  vars.acme.com                     edge.envless.cloud
TXT    _envless-challenge.vars.acme.com  envless-domain-verification=4a7d1f2c9b0e83d5461a7c2ef8b30d95a6c14f7be2093d8c5f1a6b47e9c0d2f3

TXT    Keep this record in place, we recheck it.

Add the records with proxying off. On Cloudflare that is the grey cloud, not the orange one.
Left proxied, your own Cloudflare zone answers the request, so the domain can read active while the feed never loads.

A cell the API left empty prints as -. The lines under the table are the notes the API sent with the records, one per record that carries one, and the proxying reminder, which is printed only when the record set holds a CNAME.

The rows depend on where the domain is in the flow and on what the certificate authority asks for, so copy the rows the CLI prints rather than the ones on this page. The CNAME value is the edge hostname of the deployment you are talking to, and the _envless-challenge TXT stays in place for good, because it is rechecked on every verify.

VERIFIED is yes only while the status is active. The stored verifiedAt timestamp is not what decides it, so a domain that went active and later broke on DNS or on its certificate reads no here with verifiedAt still filled in under --json.

A workspace with no domain is not an error. It prints a hint and exits 0, so a fresh workspace never fails a script:

bash
$ envless domain status
› No domain on Acme yet. Claim one with envless domain claim <hostname>.

--json prints the payload as it came back and nothing else, before that hint is considered, so a workspace with no domain prints a bare null:

bash
$ envless domain status --json
{
  "uniqueId": "0192a1b2-c3d4-7000-8000-000000000031",
  "hostname": "vars.acme.com",
  "hostnameUnicode": null,
  "status": "pending_dns",
  "records": [
    {
      "type": "CNAME",
      "name": "vars.acme.com",
      "value": "edge.envless.cloud",
      "note": null
    },
    {
      "type": "TXT",
      "name": "_envless-challenge.vars.acme.com",
      "value": "envless-domain-verification=4a7d1f2c9b0e83d5461a7c2ef8b30d95a6c14f7be2093d8c5f1a6b47e9c0d2f3",
      "note": "Keep this record in place, we recheck it."
    }
  ],
  "verificationErrors": [],
  "failureReason": null,
  "lastCheckedAt": "2026-07-28T09:12:47.000Z",
  "retryAfterSeconds": 18,
  "canReattempt": false,
  "verifiedAt": null,
  "createdAt": "2026-07-28T09:04:11.000Z"
}
bash
$ envless domain status --json
null

The JSON carries fields the table leaves out, which is where to look when a check went wrong: failureReason, verificationErrors, retryAfterSeconds and canReattempt. A failed lookup prints ✗ Could not load the domain (<error>). and exits 1.

Statuses

STATUS is the raw status string the API stores, printed as it came back:

StatusWhat it means
pending_challengeThe _envless-challenge TXT has not been seen yet. Add it, then verify.
pending_dnsOwnership is proven and the remaining records are waiting on your DNS provider.
verifyingThe records look right and the certificate is being issued.
activeServing. This is the only status that reads VERIFIED yes.
dns_errorA record does not match what the check expects.
ssl_errorThe certificate could not be issued or validation timed out.
caa_errorA CAA record on the domain blocks the certificate authority.
ownership_failedThe _envless-challenge TXT did not resolve on the last check, the first one included.
plan_graceThe plan stopped covering a custom domain, and the grace window before it is dropped is running.
blockedThe hostname was rejected upstream.
takenAnother workspace verified the same hostname first.
removingA remove ran and the certificate did not detach. Run remove again to finish it.

Verify

bash
envless domain verify
envless domain verify --reattempt

verify rechecks the records and turns the domain on once they are right. It runs the whole chain in one call: the _envless-challenge TXT first, then the hostname and certificate records, then the switch to active. There is nothing to pass, since the workspace holds one domain.

bash
$ envless domain verify
┌  envless domain verify
⚠ vars.acme.com is not verified yet (pending_dns)

TYPE   NAME                              VALUE
CNAME  vars.acme.com                     edge.envless.cloud
TXT    _envless-challenge.vars.acme.com  envless-domain-verification=4a7d1f2c9b0e83d5461a7c2ef8b30d95a6c14f7be2093d8c5f1a6b47e9c0d2f3

TXT    Keep this record in place, we recheck it.

Add the records with proxying off. On Cloudflare that is the grey cloud, not the orange one.
Left proxied, your own Cloudflare zone answers the request, so the domain can read active while the feed never loads.

│
└  ! Not verified yet. Compare the records above with your DNS provider, then run envless domain verify again. Pass --reattempt to ask for a fresh certificate check.

A run that ends anywhere short of active exits 1, which makes envless domain verify usable as a gate in a script. The records are reprinted so you can compare them against your provider without a second command.

bash
$ envless domain verify
┌  envless domain verify
✔ vars.acme.com is verified (active)
│
└  ✓ vars.acme.com is serving the exposed environments in Acme.

--reattempt throws the certificate away and asks for a new one, which is the flag for a domain stuck on ssl_error or caa_error. It changes the spinner text to Asking for a fresh certificate check, and it drops the Pass --reattempt hint from the outro. It only has an effect once a certificate has been requested, so passing it while the domain is still on pending_challenge behaves like a plain verify.

Rechecks are rate limited by the API, not by the CLI. Verifying twice inside 30 seconds returns the stored record unchanged, so the output looks like nothing moved. retryAfterSeconds under envless domain status --json says how long is left. A domain is allowed 20 verifies and 3 reattempts in total, after which the API answers Too many verification attempts. Remove the domain and add it again. or Too many retries. Remove the domain and add it again. and the run exits 1.

Verifying with nothing claimed fails on the spinner with the API message, prints Aborted. and exits 1:

bash
$ envless domain verify
┌  envless domain verify
✖ No custom domain to verify.
│
└  Aborted.

Remove

bash
envless domain remove
envless domain rm
envless domain remove --yes

remove loads the domain before it asks anything, so the confirmation names the hostname that is actually there and the spinner shows the status it is in.

bash
$ envless domain remove
┌  envless domain remove
✔ vars.acme.com (active)
│
◇  Remove vars.acme.com? Exposed environments go back to the default URL.
│  Yes
│
✔ Removed from Acme
│
└  ✓ vars.acme.com is gone. You can drop its DNS records now.

The default answer is No. Declining it, or interrupting it, prints Aborted. and exits 1, so a refusal never reads as success. Pass --yes or -y to skip the prompt.

Removing detaches the certificate and clears the hostname from the workspace, and the DNS records are yours to clean up afterwards. Nothing about your environments changes beyond the URL their feeds are served on, so envless env expose stays exactly as you left it.

A workspace with no domain stops before the prompt and exits 1:

bash
$ envless domain remove --yes
┌  envless domain remove
✖ Acme has no domain to remove.
│
└  Aborted.

Without --yes on a non-TTY the command stops before removing anything with ✗ Refusing to run unattended. Pass --yes to confirm. and exits 1. That is the flag to add in CI.

Claiming or removing a domain is also a step-up guarded action. Unless you verified a code for this workspace in the last hour, the CLI prompts for one after the confirmation, verifies it, and reruns the write once. --yes does not skip the code. Without a TTY the write fails with This action needs a verification code. Run it from an interactive terminal, or do it from the dashboard., so a job that has to run it unattended needs the workspace's Verification Codes setting turned off, see Workspace.

A refusal from the API is printed as it came back, then Aborted., and exits 1. The one to expect is The certificate could not be detached. Try again in a moment., which leaves the domain in removing and is safe to retry.

Flags

FlagUsed byDescription
--workspace <w>allWorkspace slug or unique ID. Falls back to the workspace field of .envless, then to the default set by envless workspace use.
--jsonstatusPrints the status payload as JSON and nothing else, including a bare null when no domain is claimed.
--reattemptverifyThrow the certificate away and ask for a new one. Off by default.
--yes, -yremoveSkips the confirmation prompt. Required when output is not a TTY.

Every flag is optional. --json on anything but status, --reattempt on anything but verify and --yes on anything but remove are read by nobody and change nothing.

Requirements

You must be signed in with envless login. No passphrase, ENVLESS_KEY, --product, --project or --env is needed, because these commands work on a workspace record rather than on variables.

The workspace comes from --workspace first, the workspace field of the .envless file second, and the default set by envless workspace use third. There is no environment variable for it. With none of them, the command stops on ✗ Missing --workspace. Pass it inline, run envless link, or pick a default with envless workspace use <slug|id>. and exits 1. A workspace that does not resolve prints ✗ Workspace acme not found. or ✗ Could not resolve workspace (<error>)., also exiting 1.

All four subcommands run through the write context, so a ev_sk_ machine key in ENVLESS_TOKEN is refused with ✗ Machine keys are read only. Run envless login to write from this machine. even for envless domain status. Machine keys are for reading variables, as in envless sync. See Keys.

The write context resolves a product whenever one is named, and these commands never use it. So a stale product entry in .envless, a stale ENVLESS_PRODUCT, or a --product flag pointing at something deleted makes any envless domain subcommand fail on that lookup with ✗ Product <name> not found in Acme. Clear the value or re-run envless link.

Permission is the API's call on all four subcommands, status included. Each one needs workspace update permission, and a member without it gets The user is not authorized to update this workspace. back, which status reports as ✗ Could not load the domain (The user is not authorized to update this workspace.). See envless role for which role carries it.

Exit codes

CodeWhen
0A claim landed, a status printed including the no-domain hint and a bare null, a verify came back active, a remove landed, and bare envless domain printing its usage
2Unknown subcommand, claim with no hostname, and a hostname the local check rejects
1Not signed in, machine key refused, unresolved workspace or product, any API refusal, a verify that is not verified yet, remove with no domain to remove, and a confirmation that was declined, cancelled or refused unattended

Errors

SituationBehavior
Not signed in✗ Not signed in. Run envless login first. and exit 1
ev_sk_ machine key in use✗ Machine keys are read only. Run envless login to write from this machine. and exit 1
No workspace from flag, file or default✗ Missing --workspace. Pass it inline, run envless link, or pick a default with envless workspace use <slug|id>. and exit 1
Workspace does not resolve✗ Workspace acme not found. or ✗ Could not resolve workspace (<error>). and exit 1
claim with no hostname✗ Missing the hostname. Run envless domain claim vars.acme.com. and exit 2
claim with a scheme or a path✗ Drop the scheme and any path. Pass the hostname on its own, like vars.acme.com. and exit 2
claim with a port✗ Drop the scheme and the port. Pass the hostname on its own, like vars.acme.com. and exit 2
claim with an @✗ That reads like an email address. Pass a hostname, like vars.acme.com. and exit 2
claim with a space✗ A hostname cannot hold a space. Pass one hostname, like vars.acme.com. and exit 2
claim with no dot✗ A hostname needs a dot in it, like vars.acme.com. and exit 2
Plan does not cover a custom domainFails the spinner with Custom domains need a paid plan., prints Aborted., exits 1
Deployment has no Cloudflare setupFails the spinner with Custom domains are not configured on this deployment., exits 1
Hostname refused server sideFails the spinner with the API message, for example Wildcard domains are not supported., exits 1
Hostname verified by another workspaceFails the spinner with Another workspace has verified this domain., exits 1
status lookup failed✗ Could not load the domain (<error>). and exit 1
status with no domain claimed› No domain on Acme yet. Claim one with envless domain claim <hostname>. and exit 0
verify with nothing claimedFails the spinner with No custom domain to verify., prints Aborted., exits 1
verify short of activeWarns with the status, reprints the records, exits 1
verify past its attempt capFails the spinner with Too many verification attempts. Remove the domain and add it again., exits 1
--reattempt past its capFails the spinner with Too many retries. Remove the domain and add it again., exits 1
remove with no domainFails the spinner with Acme has no domain to remove., prints Aborted., exits 1
remove declined or cancelledPrints Aborted. and exits 1, the domain stays
remove unattended without --yes✗ Refusing to run unattended. Pass --yes to confirm. and exit 1
Certificate could not be detachedFails the spinner with The certificate could not be detached. Try again in a moment., exits 1
Member without workspace updateThe API answers The user is not authorized to update this workspace. and the CLI prints it, exit 1