# Custom Domains

> **We do NOT use Cloudflare for SaaS (SSL-for-SaaS / custom hostnames).**
> New custom domains are provisioned with **certbot / Let's Encrypt** and require the
> dealer to point an **A record** (apex **and** `www`) at `SERVER_IP_ADDRESS`. There is
> no CNAME and no Cloudflare custom hostname in the current flow.

This is the authoritative doc for how dealer-owned custom domains work. If another
doc or code comment implies Cloudflare for SaaS is the current model, it is stale —
fix it and link here.

## Current model (certbot + A record)

Code of record: `lib/certbot-api.ts`, `app/api/dealer/custom-domain/route.ts`,
`app/api/dealer/custom-domain/verify/route.ts`.

1. **Configure** — `POST /api/dealer/custom-domain` writes `customDomainStatus='pending'`,
   `customDomainMethod=null`, `cloudflareZoneId=null`, and returns instructions to point an
   **A record** for the apex (and `www`) at `SERVER_IP_ADDRESS`.
2. **DNS** — the dealer creates an **A record** at their registrar. Not a CNAME.
3. **Verify** — `POST /api/dealer/custom-domain/verify` runs `verifyDnsARecord`
   (`scripts/ssl-manager.sh verify-dns`, DoH fallback) against `SERVER_IP_ADDRESS` for both
   apex and `www`.
4. **SSL** — `provisionCertificate` → `scripts/ssl-manager.sh provision <domain>` → certbot /
   Let's Encrypt SAN cert (apex + `www`) + Apache VirtualHost.
5. **Routing** — `lib/custom-domain-lookup.ts` resolves an incoming host to its dealer from the
   `Dealer.customDomain` column. No Cloudflare involved.
6. **Removal** — `DELETE` → `revokeCertificate`.

**Env vars:** `SERVER_IP_ADDRESS` (A-record target) and `CERTBOT_EMAIL`. See `.env.example`.

## Legacy: Cloudflare for SaaS (do not build on this)

A small number of pre-existing "active" domains were set up on Cloudflare for SaaS
(custom hostnames, CNAME-based). That code is **legacy-but-live** and exists only to keep
those domains working:

- `lib/cloudflare-api.ts` custom-hostname functions (`createCustomHostname`, etc.).
- Env: `CLOUDFLARE_SAAS_ZONE_ID`, `CLOUDFLARE_SAAS_API_TOKEN`, `CNAME_DOMAIN` (legacy-only).
- The `/dashboard/settings/domains/migrate` wizard + `/api/dealer/custom-domain/migrate` — a
  deprecated path that moved domains *into* the SaaS system; superseded by the certbot flow.

Do not add new features to any of the above. A domain is legacy-SaaS when
`cloudflareZoneId != null && customDomainMethod === null`.

## Not to be confused with: dealer subdomains

Dealer **subdomains** (`{subdomain}.{prefix}.{tld}`, e.g. `hbr.myamsoil.com`) are a separate
concern: on publish, the app creates **per-dealer A records** (`{subdomain}` and
`www.{subdomain}` → `SERVER_IP_ADDRESS`) in the relevant apex zone via `createDNSRecord`
(`lib/cloudflare-api.ts`, called from `app/api/dealers/[id]/publish/route.ts`). That DNS
automation is documented in [`CLOUDFLARE_SETUP.md`](./CLOUDFLARE_SETUP.md) and covers the 4 apex
zones (`myamsoil.com`, `myamsoil.ca`, `shopamsoil.com`, `shopamsoil.ca`). Those docs are **not**
about custom domains.

> **IaC note:** because these per-dealer subdomain records are created by the app at runtime,
> Infrastructure-as-Code (OpenTofu) must **not** manage them — it owns only the static zone
> config, same as it leaves custom domains alone.
