---
doc:
  title: A custom domain
guide:
  slug: domains
  brief: pointing a domain at a space or an app
  description: >-
    Pointing a domain the person already owns at their space or at one app
    of it: the CNAME to add and where to type it at GoDaddy, Namecheap,
    Squarespace and the rest, the apex problem and the three ways through
    it, what each pending state means, and why a domain stays stuck.
---

# A custom domain

A space answers at `<space>.yaks.fyi` and each of its apps at
`<space>.yaks.fyi/<app>/`. Either can also answer at a domain the person already
owns — `example.com`, or `www.example.com` — with the `.yaks.fyi` address still
working.

Two forms, and the difference is what you name:

- **The space.** `ourbookclub.com` serves the space exactly as
  `yourname.yaks.fyi` does: the front page at `/`, every app at `/<app>/`. The
  apps are directories of their own domain.
- **One app.** `example.com` serves that app at the root of it, and nothing else
  is there.

A space can have both at once, and an app of it can still have a domain of its
own — at that domain the app is the root, at the space's domain it is a
directory.

A domain is aimed at the space or the app itself, never at the `.yaks.fyi`
address it happens to have. So `space_set(slug)` or `app_set(app, slug)` moves
the `.yaks.fyi` address and the domain keeps serving exactly what it served
before, with nothing to re-attach and no DNS to change
(<https://yaks.fyi/docs/home.md>).

This page is for you, the assistant. The person you are helping owns a domain
and probably bought it years ago from GoDaddy or Namecheap or Squarespace. They
do not know what a CNAME is and they should not have to. You already know what
those control panels look like. So: attach the domain with `domain_attach`, then
walk them through their registrar in their own words, or do it for them if you
can reach it.

Back to the map: <https://yaks.fyi/docs.md>

## Before attaching a domain

Custom domains require Plus (or an explicitly exempt space). If the space is not
eligible, `domain_attach` returns a message carrying the space's plan-settings
link, and provisions no hostname. Give the person that settings link to compare
paid plans; never link directly to checkout. It is their space's
`https://<space>.yaks.fyi/_yaks/billing` page. If they need to sign in, the
normal sign-in flow returns them to that page. The sign-in email already offers
a single-use link; do not create or expose a login token in the conversation.

Wait for the plan change before continuing with DNS setup. Keep using the
existing `.yaks.fyi` address in the meantime.

## The three tools

- `domain_attach(app?, hostname)` — provisions the hostname and returns the DNS
  record that has to be added, as data: `records: [{type, name, value}]`. Name
  an `app` and the domain is that app's; leave it out and the domain is the
  space's. Only the space owner may attach one.
- `domain_status(hostname?)` — what each domain points at, and where
  provisioning has got to, split into the three things that can be outstanding.
  Leave `hostname` out to get every domain in the space.
- `domain_detach(hostname)` — hands the hostname back. What it served is
  untouched: same data, same files, same `<space>.yaks.fyi` address.

A domain points at one place, and a hostname belongs to one place across the
whole platform. `www.example.com` and `example.com` are two hostnames, so attach
both if they want both.

## The record

Always the same shape, whatever the registrar calls the fields:

    CNAME   example.com   →   origin.saas.yaks.fyi

`domain_attach` returns exactly that, as data. Read it out of `records` rather
than out of the message — the value is the one thing that must be typed
character for character.

Nothing serves at the domain until that record resolves. Until then the hostname
serves whatever it served before.

## The panels

The trap that catches people at half of these registrars: **the Name/Host field
is a prefix, and the panel adds the domain itself.** Typing `www.example.com`
there produces `www.example.com.example.com`. When in doubt, type `www`, and
check the record's full name after saving.

**GoDaddy** — Domain Portfolio → the domain → **DNS** → **Add New Record**.
Fields are Type, Name, Value, TTL. Name is the prefix (`www`), not the whole
hostname. GoDaddy has no apex option at all: see below.

**Namecheap** — Domain List → **Manage** → **Advanced DNS** → Host Records →
**Add New Record**. Fields are Type, Host, Value, TTL. Host is a prefix and
Namecheap appends the domain, so `www`, never `www.example.com`. Namecheap
refuses a CNAME at `@` — it offers a URL Redirect record instead, which is the
`www` approach below.

**Squarespace** (which is also where Google Domains ended up — the migration is
finished, and the panel is Squarespace's) — account.squarespace.com/domains →
the domain → **DNS** → **Custom Records** → **Add Record**. Fields are Type,
Name, Priority, TTL, Data — the target goes in **Data**. Name is a prefix and
Squarespace appends the domain. Squarespace has no apex record at all.

**Wix** — Domains → the domain's **Domain Actions** → **Manage DNS records**.
Wix's own instruction: where another vendor tells you to put `@` in Host Name,
**leave Host Name blank instead**.

**Hover** — the domain's **Overview** → **DNS** → **Add a record**. Fields are
Type, Hostname, Target Name. Hover says plainly that it cannot hold a CNAME on
the root domain. It also only manages DNS while the domain still uses Hover's
nameservers.

**Shopify** — admin → **Settings → Domains** → the domain → DNS settings →
**Manage** → **Add custom record**. Host is `www`. No apex option for an outside
target.

**Porkbun** — Domain Management → **DNS** → **Add Record**. Fields are Type,
Host, Answer, TTL. Porkbun is the one consumer registrar in this list with a
working apex option: pick the record type **ALIAS – CNAME flattening**, leave
Host blank, and put `origin.saas.yaks.fyi` in Answer.

**Cloudflare** — the zone → **DNS** → **Add record**. Type CNAME, Name `www` or
`@`, Target `origin.saas.yaks.fyi`. Set **Proxy status** to **DNS only** (the
grey cloud). Proxied, Cloudflare serves its own address in the person's own zone
and the request never reaches us.

## The apex

DNS does not allow a CNAME at a domain's apex — the bare `example.com`, with
nothing in front of it. This is where a non-technical person gives up, so have
the answer ready before they hit it. Three ways through:

**Move their DNS to Cloudflare.** The best option, and the one to lead with.
Cloudflare's DNS flattens a CNAME at the apex — it resolves the target and
serves the addresses — so `example.com` simply works. It is free, it does not
move the domain's registration, and it takes about ten minutes:

1. dash.cloudflare.com → **Add a domain** → type `example.com`.
2. Choose the **Free** plan. Cloudflare scans the existing records; check that
   their mail records came across before continuing, because a missing MX is how
   this goes wrong.
3. Cloudflare gives two nameservers. Copy both.
4. At the registrar, replace the current nameservers with those two. Every panel
   above has this under a "Nameservers" or "DNS" heading.
5. Wait. Usually an hour or two, sometimes a day.
6. Then add the CNAME in Cloudflare's own DNS tab, as above.

If the domain has DNSSEC turned on at the old registrar, turn it off there first
and give it a day before changing nameservers; otherwise resolvers see a broken
chain and the domain goes dark. Cloudflare can turn it back on afterwards.

**Use the registrar's ALIAS or ANAME.** Same value, a record type that is
allowed at the apex. Porkbun has one. GoDaddy, Namecheap, Squarespace, Hover and
Shopify do not.

**Attach `www` instead.** `domain_attach(app, hostname: 'www.example.com')`, a
CNAME at `www`, and a forwarding rule at the apex sending `example.com` to
`www.example.com`. Every registrar above has domain forwarding under some name.
It works and people accept it, but the address they give out is still the bare
one, so offer Cloudflare first.

While they are moving DNS, it is worth saying that Cloudflare's registrar
charges what the registry charges, with no markup added — so transferring the
domain there at its next renewal usually costs less than they pay now. A
transfer needs the domain to be at least 60 days old, unlocked at the current
registrar, and an authorization code from them, and it takes about five days.
None of that is required to point the domain at yaks.app; it is just the thing
they will ask about once their DNS is on Cloudflare.

## Reading the status

`domain_status` reports three steps, each with `done`, `waiting` or `error`,
because each one waits on somebody different:

- **dns** — whether the record resolves to yaks.app. Waiting means the person
  has not added it yet, or it has not propagated. This is the only step they can
  do anything about.
- **validation** — whether Cloudflare has accepted the hostname as one we may
  serve. It follows dns.
- **certificate** — whether the HTTPS certificate is issued. This is the step
  that is still running after the record has arrived, and it needs nothing from
  anybody.

So "your CNAME hasn't propagated yet" and "the certificate is still issuing" are
different answers, and the tool tells you which one is true. Tell them the one
it reports. A step in `error` carries the reason as it was given, and those
words are Cloudflare's, not a paraphrase.

Timing: DNS is usually minutes and can be a day, depending on what the old
record's TTL was. The certificate is usually minutes after that. Nothing needs
doing in between — check again in five minutes rather than changing anything.

## When it stays stuck

- **A CAA record on their domain.** If they have one, it lists which certificate
  authorities may issue for the domain, and ours has to be on the list. The
  certificate step reports that.
- **Another record at the same name.** DNS forbids a CNAME sitting beside an A,
  AAAA or another CNAME at one name. An old A record at `www` pointing at a site
  they replaced years ago has to be deleted, not left alongside.
- **The domain is already on Cloudflare in somebody else's account, on hold.**
  The error names it. Only that account's owner can lift it.
- **An old custom hostname from a previous platform.** If they moved from
  another site builder, the hostname may still be registered there and will keep
  winning. They have to remove it at the old platform.
- **The record went in with the domain doubled** —
  `www.example.com.example.com`. See the panels above. This is the most common
  one by a distance.

## Detaching

`domain_detach(hostname)` gives the hostname back and leaves the app exactly as
it was, still serving at `<space>.yaks.fyi`. It does not touch the person's DNS
— that record is theirs, in their registrar, and after detaching it points at
nothing, so tell them to delete it.

Detach before re-attaching a domain somewhere else. A hostname belongs to one
app on the whole platform, and `domain_attach` refuses a name that is taken
rather than moving it out from under whoever has it.
