Authio docs

Guides

Custom-domain setup wizard

Run Authio's hosted sign-in on your customer's hostname (auth.acme.com), with TLS auto-issued, and magic-link emails sent from their domain.

Custom domains lift Authio off the platform-default hostname. Customers see auth.acme.com instead of auth.authio.com; magic-link emails arrive from noreply@acme.com; and the panel is fully branded. This guide walks through the wizard in the dashboard, then the DNS work at the customer's registrar, then the verify-cert-activate sequence.

Concept-level background and the data model live at Custom domains, branded email, and redirect URIs. This page is the operator walkthrough.

Custom domains are an Enterprise-plan feature. Lower-tier tenants see an Available on Enterprise · Contact sales CTA in place of the CRUD UI.

1. Add the hostname in the Authio dashboard

Sign in as an owner or admin of the project, then open dashboard / Settings / Custom domains and click Add domain. Enter the hostname your users will land on — typically auth.acme.com.

Authio writes a row to custom_domains with status = pending and cert_status = not_attempted. The hostname is globally unique across all projects — adding a host another project already owns returns 409 domain_in_use.

[screenshot: dashboard / Settings / Custom domains / Add domain modal]
The new-domain modal accepts one fully-qualified hostname per row. Wildcards aren't supported — add each subdomain you want to use.

2. Copy the DNS records Authio surfaces

Once the row is created, the dashboard renders the records the customer needs to publish at their DNS provider:

  • Routing CNAME — points the customer's hostname at Authio's Cloudflare-for-SaaS fallback origin. Example: auth.acme.com CNAME custom.authio.com. Always required.
  • Ownership TXT — proves the customer controls the domain (catches typo-squatting). Example: _authio-verify.auth.acme.com TXT "authio-verify=acme-9f3b...4421". Always required.
  • SSL DCV records — domain control validation records Cloudflare uses to issue the Let's Encrypt cert. Sometimes a TXT, sometimes a CNAME under _acme-challenge.…. Shown only when the platform Cloudflare zone has SSL for SaaS enabled.
  • CAA record (sometimes) — required only if the customer's root domain already publishes a CAA record that doesn't list Let's Encrypt (letsencrypt.org). If so, the dashboard will surface a CAA addendum: acme.com CAA 0 issue "letsencrypt.org".

Each row has a one-click copy affordance next to both the host and the value. Hand these to whoever runs DNS at the customer.

3. Add the records at the customer's registrar

DNS UX varies by provider; the records themselves are portable.

Cloudflare

DNS → Records → Add record. Crucial: the routing CNAME must have the orange-cloud proxy disabled (DNS-only). If the proxy is on, Cloudflare for SaaS will see a Cloudflare IP for the routing CNAME and reject validation.

Route 53

Hosted zones → acme.com Create record. Choose Simple routing, record type CNAME (or TXT), paste the value Authio gave you. Route 53's default TTL of 300 seconds is fine.

GoDaddy / Namecheap / others

Look for “DNS Management” or “Advanced DNS.” The pattern is the same: hostname + type + value. Some registrars suffix the apex domain to host names; if so, the host field for auth.acme.com is just auth.

4. Click Verify in the dashboard

Back in Settings / Custom domains, click Verify now on the row. Authio runs three concurrent checks:

  • CNAME lookup against the customer's hostname. The dashboard's status moves to verified once the CNAME resolves to custom.authio.com (or the Cloudflare for SaaS equivalent in your region).
  • TXT lookup on _authio-verify.…. The token from the row must be the exact TXT value.
  • Cloudflare for SaaS pre-check. If SSL for SaaS is enabled, Authio registers the hostname with Cloudflare and starts the cert flow; the row moves to cert_status = cert-pending.

Status transitions you'll see, in order:

  • pending → the row exists, DNS records surfaced, but the customer hasn't published them yet (or hasn't clicked Verify).
  • verified → CNAME + TXT both passed. Routing works; if SSL-for-SaaS is off, this is also the terminal state.
  • cert-pending → Cloudflare is issuing the cert. Typically 1–5 minutes; rarely up to 15.
  • active → cert is live; the customer's users can sign in at https://auth.acme.com/….
The audit-worker re-runs verification every 60 seconds for rows in pending or cert-pending, so the dashboard updates without anyone clicking Verify again once DNS propagates.

5. What changes once the domain is active

From the cutover moment, every reference to the customer's sign-in surface points at their domain:

  • Hosted-UI is served from https://auth.acme.com/login, etc.
  • OAuth redirect URIs and callbacks are emitted under auth.acme.com.
  • Magic-link email URLs contain Acme's domain — users never see authio.com in the URL they click.
  • Webhooks the customer receives still come from Authio (we don't spoof your domain on outbound traffic), but the embedded sign-in URL inside the payload uses the custom-domain hostname.
  • WebAuthn passkeys are scoped to auth.acme.com via RPID — see WebAuthn RPID for custom domains before the customer enrolls passkeys here.

Optional: enable branded email

From the domain detail page, click Enable branded email with a from-address like noreply@acme.com. Authio mints a per-customer SES identity and surfaces three DKIM CNAMEs to publish. Click Verify DKIM after publication. Once SES marks DKIM SUCCESS (usually a few minutes to ~24 hours), sign-in emails go out from Acme's domain immediately. See the branded-email setup guide for full details.

Troubleshooting

My CNAME isn't propagating

Test from a few independent resolvers:

# Public resolvers
dig +short CNAME auth.acme.com @1.1.1.1
dig +short CNAME auth.acme.com @8.8.8.8

# Authoritative resolver for the customer's zone
dig +trace CNAME auth.acme.com

The expected answer is custom.authio.com.followed by Cloudflare's IPs. If different resolvers return different answers, the customer's DNS is in propagation; wait one TTL (often 300 s) and click Verify again.

Common gotchas:

  • On Cloudflare, the orange-cloud proxy was left on. Toggle to DNS-only and re-verify.
  • The customer published www.auth.acme.com instead of auth.acme.com. Compare host fields exactly.
  • The customer has an existing record at the same hostname (an old A record from a prior auth provider). DNS only allows one CNAME per host — delete the old record first.

The cert has been pending for >15 minutes

Three causes, in order of frequency:

  1. CAA blocks Let's Encrypt. Run dig CAA acme.com. If the response lists any issuer that isn't letsencrypt.org and no explicit allow for it, ask the customer to add acme.com CAA 0 issue "letsencrypt.org" and re-verify.
  2. DCV records missing. If SSL for SaaS asked for an _acme-challenge.auth.acme.com CNAME and the customer skipped it, the cert won't issue. Check the dashboard row — the missing-records badge surfaces this.
  3. Cloudflare for SaaS not enabled on the platform zone. The dashboard surfaces “CF SaaS pending” with cert_status = not_attempted. DNS verification still works (status reaches verified), but the cert never starts. Contact Authio support to flip on SSL for SaaS, then re-verify.

I get “Hostname owned by another tenant”

That hostname already has a custom_domains row in another project. Either the same customer registered it under a different Authio project (route them to the right one) or a previous test left the row behind (Authio support can release it via the admin app).

WebAuthn passkeys stopped working after I added the domain

Passkeys are RPID-bound to the origin they were enrolled at. Users who enrolled on auth.authio.com can't reuse them on auth.acme.com — different RPID, by spec. The hosted-UI handles this by re-prompting for enrollment on the new domain; the user signs in once via magic-link, then enrolls a new passkey. Plan the cutover accordingly — see WebAuthn RPID for custom domains.

Changing or removing a custom domain

Custom domains aren't a permanent commitment, but moving them has user-visible consequences. The supported flow:

Add the new domain alongside the old one

Custom-domain rows are independent. You can run auth.acme.com and auth-v2.acme.com in parallel for as long as you need. New sign-ins arriving at the old domain still work until you remove its row.

Migrate magic-link URLs over time

Authio renders magic-link URLs against the project's default custom domain (the first active row). To flip the default, set is_default = true on the new row via the dashboard (Settings → Custom domains → Set as default). The old domain still resolves; new emails point at the new domain.

Plan a passkey re-enrollment window

This is the only non-trivial step. Tell affected users in advance that they'll be asked to re-enroll. The hosted- UI's “Set up sign-in” flow handles the actual enrollment; you just need to set expectations.

Decommission the old domain

Once the new domain has carried the bulk of sign-in traffic for a billing cycle and passkeys have re-enrolled, the operator can delete the old custom_domains row from the dashboard. Authio releases the Cloudflare-for-SaaS hostname, the cert is decommissioned, and a final custom_domain.removed audit event lands.

Deleting a custom-domain row that's still receiving traffic returns sign-in attempts to auth.authio.com. If the customer's app hard-codes the custom hostname (rather than reading from the Authio config endpoint), those sign-ins will fail until the customer redeploys. Always migrate to the new domain first; delete second.

See also