Skip to main content

Your users' domains

Give each app a name under your wildcard domain, and connect domains your users bring, with the project domains API.

Each app can answer on names under your wildcard domain, on domains your users own, or both. You add both kinds the same way, with POST /v1/projects/:id/domains, and get one response shape back that has everything your UI needs to show.

Assign a name under your wildcard domain

Once your wildcard domain is live, any name one label under it is live as soon as you add it to a project. There are no records to add and no certificate to wait for:

const domain = await easel.domains.add(project.id, {
  name: "trip-planner.yourplatform.app",
});
// domain.status === "live", domain.records is empty

You can choose the name for each app, let your users pick it, or both. When a user picks it, check it in your backend before you call the API: allowed characters, names you reserve, such as www or admin, and names you don't want on your domain.

  • The name serves the project's Current Production deployment, and follows every promote and rollback.
  • Only names one label under the wildcard are covered. a.trip-planner.yourplatform.app is an ordinary custom domain and needs its own DNS record.
  • A name assigned before the wildcard domain is live goes live with it.
  • To rename an app, add the new name, then remove the old one.

Connect a user's own domain

When a user brings a domain, add it to their app's project. The response says what to add at their DNS provider:

{
  "domain": {
    "name": "trips.example.com",
    "projectId": "cmgq3x7lk0001ab12cd34ef56",
    "status": "pending_dns",
    "records": [
      { "type": "CNAME", "name": "trips", "value": "domains.yourplatform.app" }
    ],
    "error": null,
    "verifiedAt": null,
    "createdAt": "2026-10-16T09:30:00.000Z"
  }
}

Show your user the record. For a subdomain, it's a CNAME to your branded target, or to the domain's own Easel target, such as d-3f9c2a71b0de5a17.cname.easelusercontent.com, if you haven't set one. The target stays with the domain for as long as it exists. For a root domain, such as example.com, it's an A record to Easel's edge. Subdomains are easier for most users; see Configure DNS for the details of each.

Easel checks pending domains every minute, then issues the certificate. To check now, for example when your user selects a Check now button, call domains.verify(). It waits briefly for the certificate and returns the domain in its new state.

Statuses

statusMeaningShow your user
pending_dnsThe record is missing or doesn't match yetThe record in records, and error if set
provisioning_tlsDNS is right; the certificate is being issuedThat it's almost ready
liveThe domain serves the app over HTTPSThe live URL
errorSetup failederror, which says what's wrong

records is empty once the domain is live. Each change also sends a domain.updated webhook, so you can update your UI without polling.

A domain someone else holds

If another Easel workspace holds the domain, adding it fails with 409, the code domain_in_use, and the record that proves your user controls it, in records. For a subdomain, that's a CNAME to a target Easel creates for this claim, not your branded target; for a root domain, it's a TXT record:

{
  "error": "example.com is in use by another workspace. Add the DNS record shown to prove you control it, then try again.",
  "code": "domain_in_use",
  "hosts": ["example.com"],
  "records": [
    { "type": "TXT", "name": "_easel.example.com", "value": "easel-verify=example.com,<token>" }
  ]
}

Show the record, and once your user has added it, add the domain again. See Domain ownership for how ownership moves.

Other errors when adding a domain

StatuscodeMeaning
409domain_in_projectThe name is on another project in your workspace. Move it
403wildcard_domain_not_yoursThe name is under another workspace's wildcard domain
400invalid_domainThe name isn't a valid hostname

Adding a name returns 201. Adding a name the project already has returns it with 200, so retrying is safe.

Move a domain between apps

To move a name from one of your projects to another, such as when a user replaces an app, send the same request to the new project with move: true:

await easel.domains.add(newProject.id, { name: "trips.example.com", move: true });

The domain keeps its CNAME target, status, certificate, and redirect settings, so your user doesn't change DNS. A move returns 200. A token limited to projects has to reach both projects, or the request fails with 403 and token_scope.

Remove a domain

DELETE /v1/projects/:id/domains/:domain stops serving the name right away. For a domain your user brought, tell them to delete the DNS record too. See Removed domains for how long a removed root domain stays reserved for your workspace.

Build the UI

The reference app's custom domain block has both modes, a subdomain of your wildcard domain and My own domain, and shows records, statuses, and the ownership record. See UI blocks.

Next steps