← Files Website Deploy ToolkitARCHIVED FILE

skills/connect-domain/references/registrars.md

12.4 KB · Oct 4, 2026 · 12:33 UTC

↓ Download file

# Adding the DNS records at the registrar

Companion to the `connect-domain` skill, step 3 / 3b. The records are always the two from the
bind response: **CNAME `<label>` → `cname.simple-host.app`** for a subdomain, or **A `@` →
the IP the bind returned** for an apex, and the **TXT ownership record** (`dns_txt`):
**TXT `_simple-host.<label>`** (apex: `_simple-host`) holding the site's token
(`sh-` + 32 characters). Nothing else changes. Ask the user which service hosts
the domain's DNS (it is the registrar unless they moved nameservers — `dig NS <zone> +short`
tells you), then follow that section.

Rules that apply everywhere:

- **Never CNAME an apex.** A CNAME at `@` breaks MX/TXT/everything at the root and most
  registrars refuse it. Apex = A record (or ALIAS/ANAME where the provider offers one, pointed
  at `cname.simple-host.app`).
- **The TXT record is added the same way as the address record,** with type `TXT`, the name
  from `dns_txt.host` (label form: drop the zone, e.g. `_simple-host.recipes`) and the value
  exactly as given (some panels add the quotes themselves). It does not replace any existing TXT
  record, and it stays in place after the domain is live. Every API call below takes it with
  the same body, `type` `TXT`.
- **TTL 60–300 s while connecting.** Low TTL means a typo is fixable in minutes, not an hour.
  Raise it later if the user cares; the server doesn't.
- **Add, don't replace** — except at the apex, where a parking/default A or ALIAS at `@`
  already exists and must be **edited** (or removed and re-added), not duplicated. Leave MX,
  TXT, everything else alone.
- **Credentials:** if the user hands you an API key, use it for the one call (plus a read-back
  to confirm) and forget it. Never write it into the site, the repo, a config file, or a
  message. Ask permission naming the exact record before the write (skill step 3b).
- **Check it landed at the authoritative server, not a public resolver.** `8.8.8.8` / `1.1.1.1`
  may hold an older answer (often a parking wildcard) for the old TTL — a "wrong" answer there
  right after the change means cache, not a mistake. Query the registrar's nameserver directly.

---

## Vercel DNS

Applies when the domain's nameservers are `ns1.vercel-dns.com` / `ns2.vercel-dns.com`
(domains bought at Vercel, or moved there). If the domain is in a Vercel *project* but its
nameservers are elsewhere, Vercel is not the DNS host — go to the actual host.

**UI:** Vercel dashboard → team → **Domains** (sidebar) → click the domain → DNS Records form
(**Name**, **Type**, **Value**, **TTL**; **Enable Vercel DNS** first if the form is hidden) → **Add**.

- Subdomain: Name `recipes` · Type `CNAME` · Value `cname.simple-host.app` · TTL `60`.
- Apex: Name *empty* · Type `A` · Value `<ip from bind>` · TTL `60`. Vercel also supports
  `ALIAS` at the apex (Value `cname.simple-host.app`) — either works; the A record is the one
  the bind response describes, so prefer it. Don't leave both.
- Name is the **label only** (`recipes`, not `recipes.brand.com`). Apex = empty string in the
  API, blank in the UI. Default and minimum TTL is 60.

**API** (verified 2026-09-06 against
https://vercel.com/docs/rest-api/reference/endpoints/dns/create-a-dns-record ; the docs list
`/v2`, `/v4` is what worked in practice the same day — both accept this body):

```bash
# token: Vercel → Account Settings → Tokens. teamId only if the domain belongs to a team.
curl -sS -X POST "https://api.vercel.com/v4/domains/$ZONE/records?teamId=$TEAM_ID" \
  -H "Authorization: Bearer $VERCEL_TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"recipes","type":"CNAME","value":"cname.simple-host.app","ttl":60}'
# apex:  -d '{"name":"","type":"A","value":"<ip from bind>","ttl":60}'
# → {"uid":"rec_..."}   keep uid: undo is
#   curl -X DELETE "https://api.vercel.com/v2/domains/$ZONE/records/$UID?teamId=$TEAM_ID" -H "Authorization: Bearer $VERCEL_TOKEN"
# list:  GET https://api.vercel.com/v4/domains/$ZONE/records?teamId=$TEAM_ID
```

`403` = token is personal but the domain is on a team (add `teamId`), or the token lacks the
scope. `409` = a record with that name/type already exists — list, then delete or edit it.

**Check it landed:** `dig +short recipes.brand.com @ns1.vercel-dns.com`
(apex: `dig +short A brand.com @ns1.vercel-dns.com`). Vercel answers within seconds.

---

## GoDaddy

**UI:** godaddy.com → sign in → **Domain Portfolio** (My Products → Domains) → click the domain →
**DNS** tab → **Add New Record**.

- Subdomain: Type `CNAME` · Name `recipes` · Value `cname.simple-host.app` · TTL: pick the
  smallest offered (default is 1 hour; choose 600 s / "custom" if the menu allows it).
- Apex: GoDaddy has **no ALIAS/ANAME** — the A record is the only option. A default A record
  at `@` (parking, "WebsiteBuilder Site", or "Forwarding") is already there: **edit** it (pencil
  icon) to the IP from the bind rather than adding a second one. If a *forwarding* is set up
  on the domain, turn it off under **Forwarding** or it fights the A record.
- Name is the label only; `@` means the apex. Save; GoDaddy's own nameservers pick it up in
  under a minute.

**API** — works for any account that holds at least one active domain (GoDaddy policy,
https://www.godaddy.com/help/how-do-i-access-domain-related-apis-42424, read 2026-09-06): the
Domains API, which is the DNS one, is capped at 20,000 calls/month; only bulk *availability*
checks need 50+ domains, and we never use those. Auth is a **Personal Access Token** from
developer.godaddy.com (`Authorization: Bearer <PAT>`, scope `domains.dns:update`); legacy keys use
`Authorization: sso-key <KEY>:<SECRET>` and still work on v1. A `403` therefore means the token
lacks the DNS scope, belongs to a different account, or the domain is not in that account — check
those, then if it still fails do the record in the UI.

Endpoints verified 2026-09-06 against https://developer.godaddy.com/openapi/domains-v1.json
(the landing page https://developer.godaddy.com/doc/endpoint/domains now fronts a v3 API whose
DNS calls live at `POST /v3/zones/{zone}/dns-records` — same body fields; v1 below is the
long-standing one). Base URL `https://api.godaddy.com`. Records are arrays; `name` is the label
(`@` for apex); v1 types are `A AAAA CAA CNAME MX NS SOA SRV TXT` (no ALIAS). GoDaddy has
historically rejected `ttl` under 600 — the spec doesn't state a minimum, so use 600.

```bash
# add (does not touch existing records) — subdomain CNAME
curl -sS -X PATCH "https://api.godaddy.com/v1/domains/$ZONE/records" \
  -H "Authorization: Bearer $GODADDY_PAT" -H 'Content-Type: application/json' \
  -d '[{"type":"CNAME","name":"recipes","data":"cname.simple-host.app","ttl":600}]'

# apex: REPLACE the existing A records at @ (PATCH would add a second A next to the parking one)
curl -sS -X PUT "https://api.godaddy.com/v1/domains/$ZONE/records/A/@" \
  -H "Authorization: Bearer $GODADDY_PAT" -H 'Content-Type: application/json' \
  -d '[{"data":"<ip from bind>","ttl":600}]'

# read back / undo
curl -sS "https://api.godaddy.com/v1/domains/$ZONE/records/CNAME/recipes" -H "Authorization: Bearer $GODADDY_PAT"
curl -sS -X DELETE "https://api.godaddy.com/v1/domains/$ZONE/records/CNAME/recipes" -H "Authorization: Bearer $GODADDY_PAT"
```

Success is an empty `200`/`204` body. `PUT .../records/{type}/{name}` replaces **all** records
of that type at that name — only ever use it for the apex A, never for `TXT`/`MX`.

**Check it landed:** GoDaddy nameservers vary by account (`ns23.domaincontrol.com`,
`ns68.domaincontrol.com`, ...) — find them first:
```bash
dig NS brand.com +short            # e.g. ns23.domaincontrol.com.
dig +short recipes.brand.com @ns23.domaincontrol.com
```

---

## Porkbun

**UI:** porkbun.com → sign in → **Account → Domain Management** → find the domain → **Details**
(or the **DNS** link on the row) → **DNS Records** (edit icon) → **Add Record**.

- Subdomain: Type `CNAME` · Host `recipes` · Answer `cname.simple-host.app` · TTL `600`.
- Apex: Type `A` · Host *blank* · Answer `<ip from bind>` · TTL `600`. Porkbun also offers
  `ALIAS` at the apex (Answer `cname.simple-host.app`); either is fine — prefer the A record
  the bind described.
- **New Porkbun domains ship with default parking records** (typically an `ALIAS` at the root
  and a `CNAME` at `*`) — the error *"A CNAME or ALIAS record with that host already exists"*
  means one is in the way. Delete only the record at the **same host** you are adding
  (root for apex; `*` never needs touching for a named subdomain). Nothing else.
- Host is the label only (blank = root). Porkbun's minimum TTL is 600 by default (account
  setting) — 600 is fine; the 60–300 advice above is "as low as the provider allows".

**API** (verified 2026-09-06 against https://porkbun.com/llms-full.txt, the text version of
https://porkbun.com/api/json/v3/documentation). Base URL **`https://api.porkbun.com/api/json/v3`**
(the old `porkbun.com` host is retired). Two prerequisites, both in the UI, both per user:

1. Keys: **Account → API Access → Create API Key** → `pk1_...` + `sk1_...` (secret shown once).
2. **Per-domain opt-in:** Domain Management → the domain's **Details** → **API Access** toggle
   ON. Without it every call for that domain fails (the error's `next_action` says "enable API
   access"). Ask the user to flip it; you cannot do it via API.

Credentials go in the JSON body (`apikey`, `secretapikey`) or as headers
`X-API-Key` / `X-Secret-API-Key`. Writes are `POST`. `name` is the label (blank/omitted =
root); `content` is the value; `ttl` minimum is the account minimum (typically 600). Pass
`"dryRun": true` first to rehearse without writing — Porkbun validates ownership/permissions and
returns `wouldSucceed`.

```bash
# ping — proves the key pair works before touching anything
curl -sS -X POST https://api.porkbun.com/api/json/v3/ping \
  -H 'Content-Type: application/json' -d "{\"apikey\":\"$PB_KEY\",\"secretapikey\":\"$PB_SECRET\"}"

# subdomain CNAME
curl -sS -X POST "https://api.porkbun.com/api/json/v3/dns/create/$ZONE" \
  -H 'Content-Type: application/json' \
  -d "{\"apikey\":\"$PB_KEY\",\"secretapikey\":\"$PB_SECRET\",\"name\":\"recipes\",\"type\":\"CNAME\",\"content\":\"cname.simple-host.app\",\"ttl\":600}"
# apex A:  ...,"name":"","type":"A","content":"<ip from bind>","ttl":600}
# → {"status":"SUCCESS","id":"123456789"}   keep id: undo is
#   POST https://api.porkbun.com/api/json/v3/dns/delete/$ZONE/$ID  (same credentials body)

# read back
curl -sS -X POST "https://api.porkbun.com/api/json/v3/dns/retrieve/$ZONE" \
  -H 'Content-Type: application/json' -d "{\"apikey\":\"$PB_KEY\",\"secretapikey\":\"$PB_SECRET\"}"
```

For the apex, if a parking `ALIAS` at root blocks the `A`, `dns/retrieve` gives its `id`;
`dns/delete/$ZONE/$ID` it, then create. `status: "ERROR"` with a `code` is the failure form;
HTTP 429 carries `Retry-After`.

**Check it landed:** Porkbun nameservers vary (`curitiba.ns.porkbun.com`,
`fortaleza.ns.porkbun.com`, `maceio.ns.porkbun.com`, `salvador.ns.porkbun.com` are common) —
find them first:
```bash
dig NS brand.com +short            # e.g. curitiba.ns.porkbun.com.
dig +short recipes.brand.com @curitiba.ns.porkbun.com
```

---

## Any other registrar / DNS host

Same two records, same rules. Work out where DNS actually lives (`dig NS <zone> +short` — Cloudflare,
Route 53, Namecheap, Google/Squarespace, etc. all show up here), then:

- **UI:** the DNS editor is under the domain's settings, usually named *DNS*, *DNS Records*,
  *Manage DNS*, *Advanced DNS* or *Zone*. Fields map to Type / Name-Host (label only, `@` or
  blank for apex) / Value-Target-Answer-Points to / TTL. Some hosts (Namecheap, older panels)
  want the target with a trailing dot: `cname.simple-host.app.` is always safe.
- **Cloudflare specifically:** set the record to **DNS only** (grey cloud), not proxied — a
  proxied record hides the origin from our verifier and the cert check. Cloudflare flattens
  CNAME at the apex, so there a `CNAME @ → cname.simple-host.app` is acceptable.
- **API:** only if the user offers credentials and you can find the vendor's current docs;
  otherwise hand over the record. Same rule: one write, a read-back, forget the key.
- **Check it landed:** `dig NS <zone> +short`, then `dig +short <host> @<that nameserver>` and
  `dig +short TXT _simple-host.<host> @<that nameserver>`.
  Only after the authoritative server answers correctly is a wrong answer from `8.8.8.8` a
  cache issue worth waiting out (the old TTL, worst case an hour or a day).

SHA-256: 56ffc6395d295c25fe05d349e22e77bc8098424a07fc71d992e226e67885c562