Every service gets a platform hostname automatically on deploy ({service}.tawa.pro,
{service}.sandbox.tawa.pro, {service}.uat.tawa.pro). To put your own domain in front of a
service, use tawa domain. There are three DNS modes, picked by how the domain's DNS is
managed and whether Cloudflare can be used.
--cloudflare | --external (Cloudflare for SaaS) | --direct (BYO-DNS) | |
|---|---|---|---|
| Customer DNS record | CF auto-creates CNAME | One CNAME → customers.tawa.pro, client keeps their DNS | A record → ingress LB IP |
| TLS terminated at | Cloudflare edge | Cloudflare edge (custom hostname) | In-cluster (cert-manager / Let's Encrypt) |
| Apex/root domains | CF flattening | not supported (use --direct or forward to www) | native (A record is legal at apex) |
| CDN / WAF / DDoS | yes | yes | no (origin hit directly) |
verify checks for | n/a (auto) | custom hostname + cert status, and CNAME → customers.tawa.pro | A → ingress LB IP |
| Needs CF zone / NS on CF | yes | no | no |
Default to --cloudflare for domains we manage; use --external when a client owns the domain and keeps their DNS. Use --direct only when the customer cannot move their
nameservers to Cloudflare (e.g. DNS stuck at HostGator/cPanel) but can still set an A record.
# Cloudflare-managed (auto DNS + TLS) — preferred
tawa domain add portal.example.com --cloudflare
# Client-owned domain (client adds one CNAME to customers.tawa.pro; TLS at our edge)
tawa domain add portal.example.com --external
# Direct A record (BYO-DNS, TLS issued in-cluster) — for when Cloudflare is not an option
tawa domain add example.com --direct
tawa domain verify example.com
tawa deploy --prod
tawa domain list
tawa domain status example.com
tawa domain remove example.com
--cloudflare writes the CNAME for you, which makes it easy to aim at a name that already
exists. Since iec-builder#163, a first-time add refuses when Cloudflare already has a record
for that name pointing somewhere other than this service:
409 DNS_RECORD_EXISTS
A CNAME record for 'bio.tawa.pro' already exists and points at '…', not this service.
Refusing to replace it.
Before that fix the builder updated the record in place (or deleted and recreated it on a type
change), so tawa domain add bio.tawa.pro --cloudflare, run from any service, would have
repointed Bio-ID. Platform hostnames and hand-made records (builder.tawa.pro, git.tawa.pro)
are not custom domains, so the older DOMAIN_TAKEN check never covered them.
Only address records — CNAME, A and AAAA — count. A TXT, MX, CAA or NS record at the same name is not something a custom domain replaces, so it neither blocks the add nor gets touched by it (iec-builder#236).
| Existing records at the name | Result |
|---|---|
| none, or only TXT / MX / CAA / NS | created; the other records are left alone |
| CNAME already pointing at this service's target | proceeds, idempotent |
| CNAME to a replaceable target (below) | replaced; response carries replacedRecord: { type, content } |
| any other CNAME / A / AAAA | 409 DNS_RECORD_EXISTS, record untouched |
| (re-adding a domain this service already owns) | resync, unchanged behaviour |
Replaceable targets (iec-builder#270). Some records are placeholders that exist precisely so
a real service can take the name over: a parked www CNAMEd at the old marketing host, or at the
apex it sits under. An address record is replaceable, not foreign, when its target is
DNS_REPLACEABLE_TARGETS (builder env, comma-separated; default
sites.insureco.io,hypesites.tawa.pro), orwww.clubmvp.co CNAME clubmvp.co) — except inside the platform domain.Any foreign record at the name still wins: a name with one replaceable and one foreign record
is refused. A record pointing at another tawa.pro service hostname is never replaceable
unless that hostname is explicitly listed.
If replacing the record really is the point, remove it first, or call POST /domains/add with
"overwriteExistingRecord": true. Overwriting replaces only the conflicting address
records (a CNAME replaces A/AAAA; an A or AAAA replaces a CNAME). TXT, MX, CAA and NS records
are never deleted — if Cloudflare refuses the new record because of one, the add fails with
Cloudflare's error and nothing is removed.
Correction (2026-10-03): before iec-builder#236 the builder looked up "the" record at a name without regard to its type and used whichever Cloudflare listed first. An apex carrying a
google-site-verificationTXT was therefore read as a foreign record, andtawa domain add <apex> --cloudflarealways failed with409 DNS_RECORD_EXISTS. WithoverwriteExistingRecord: truethe same bug would have deleted the TXT to make room. policyblockchain.com was set up on 2026-10-03 around it, withtawa domain add --external, CNAMEs created by hand in Cloudflare, anddnsVerifiedset directly in the builder DB. Do not repeat that workaround;--cloudflarenow works on such an apex.
tawa domain verify for --external (and --cloudflare) looks for a public CNAME to the
expected target. A proxied CNAME, and every CNAME at an apex (Cloudflare flattens it),
never answers a CNAME query — the name resolves to Cloudflare edge addresses instead. So when
the domain resolves but shows no CNAME, verify asks Cloudflare directly and passes if:
active (its nameservers point at
Cloudflare), andresolvedTarget then reads CNAME: <target> (proxied/flattened by Cloudflare). A zone outside
this Cloudflare account still needs a public CNAME.
A Cloudflare-mode add also patches the service's live ingress and syncs its OAuth redirect URIs, so the host serves traffic without a redeploy. The domain is stored on the service, and later deploys keep it.
--cloudflare used to fail with No Cloudflare zone found… for any domain whose zone was not
already in the platform's Cloudflare account. Since iec-builder#270 the builder creates the
zone (type full) for the registrable apex and writes the CNAME into it: www.example.com
creates example.com; known two-part suffixes are respected (shop.example.co.uk creates
example.co.uk). The account is CLOUDFLARE_ACCOUNT_ID, else the token's first account.
The new zone is pending until the registrar delegates the domain to Cloudflare. The add
response says so, and carries what to do:
{ "dnsVerified": true,
"zone": { "id": "…", "name": "clubmvp.co", "status": "pending", "created": true,
"nameServers": ["ada.ns.cloudflare.com", "bob.ns.cloudflare.com"],
"originalNameServers": ["ns1.hostgator.com", "ns2.hostgator.com"] },
"message": "DNS is configured, but clubmvp.co is not live yet: set its nameservers at the registrar to ada.ns.cloudflare.com, bob.ns.cloudflare.com (currently ns1.hostgator.com, ns2.hostgator.com)." }
dnsVerified is true because the record is in place; the site is not reachable until the
nameservers change. Set them at the registrar, then re-run the add (or check
GET /domains/:domain/status, which reads the zone live and returns zoneStatus and
nameServers). zone is present on every Cloudflare-mode add, with created: false for a zone
the account already held. The stored domain record keeps zoneStatus and nameServers.
--external, Cloudflare for SaaS)When a client owns the domain and keeps their DNS at their own registrar, --external
registers a Cloudflare custom hostname (Cloudflare for SaaS, DV certificate by HTTP
validation). The client changes nothing but one record. TLS terminates at our Cloudflare
edge; no nameserver change, no Cloudflare account for the client.
tawa domain add portal.client.com --external
# → Ask the domain owner to add CNAME portal.client.com → customers.tawa.pro.
# Optional, to issue TLS before switching: TXT _cf-custom-hostname.portal.client.com = <value>.
tawa domain verify portal.client.com # re-run until state is "active"
| State | Meaning | Outstanding |
|---|---|---|
pending-cname | The CNAME does not resolve to customers.tawa.pro yet | the CNAME (and optional TXT) |
pending-validation | CNAME is in place; the custom hostname or certificate is not active yet | validation records, if any |
active | CNAME resolves and the certificate is issued; dnsVerified is set | none |
verify and status read the live custom hostname and certificate status from Cloudflare
and the CNAME from public DNS. dnsVerified becomes true only at active.add; traffic only arrives once the client's CNAME exists.add returns 400 EXTERNAL_APEX_UNSUPPORTED. Use --direct (an A record), or forward the apex to www at the
registrar and add the www hostname.add and verify return 502 SAAS_UNAVAILABLE with the Cloudflare error. There is
no silent fallback.tawa domain verify <domain>, which creates the custom hostname.tawa domain remove deletes the custom hostname.Builder settings: SAAS_ZONE_ID (default CLOUDFLARE_ZONE_ID), SAAS_CNAME_TARGET (default
customers.tawa.pro, kept as a proxied record at the ingress and set as the zone's fallback
origin automatically), SAAS_ENABLED (false restores the record-only behaviour).
tawa domain add <domain> --direct registers the domain on the service with
dnsProvider: 'direct'. The expectedTarget is the cluster ingress LoadBalancer IP
(INGRESS_TARGET on the builder). The CLI prints the A record to add.www, registered separately) pointing
at that IP. Apex domains work natively — no CNAME flattening needed.tawa domain verify <domain> checks that the A record resolves to the ingress LB IP
(not a CNAME). On success the record is marked dnsVerified.tawa deploy injects the verified direct-mode hosts into the service's builder-managed
ingress, adds a tls block (secretName: {service}-custom-tls) and the annotation
cert-manager.io/cluster-issuer: {CERT_MANAGER_CLUSTER_ISSUER} (default letsencrypt-prod).Cloudflare/external domains are not given an in-cluster TLS block — Cloudflare terminates
their TLS. Only direct domains get cert-manager TLS.
ClusterIssuer named to match
CERT_MANAGER_CLUSTER_ISSUER (default letsencrypt-prod, HTTP-01 solver on ingressClassName: nginx).INGRESS_TARGET on the builder must be set to the ingress LB IP. tawa domain add --direct
fails with DIRECT_MODE_UNAVAILABLE if it is unset or not an IP.tls block.Direct mode registers one record per hostname. For apex + www, add both:
tawa domain add example.com --direct
tawa domain add www.example.com --direct
tawa domain verify example.com && tawa domain verify www.example.com
tawa deploy --prod
Both hosts are added to the same {service}-custom-tls secret, so a single Let's Encrypt cert
covers them.
Last updated: October 6, 2026