Concepts
Custom domains, branded email, and redirect URIs
The Enterprise tier lifts Authio off the platform-default hostname. Customers see auth.acme.com instead of auth.authio.com, sign-in emails come from noreply@acme.com, and the panel is fully branded.
Part of Authio Lobby
Custom domains are a three-layer stack on top of the same custom_domains table. Each layer is independently opt-in, but the most common flow lights up all three together.
L1 — Auth subdomain
The customer points a hostname they own (typically auth.acme.com) at Authio with a CNAME. Authio registers the hostname with Cloudflare for SaaS, which issues a free Let's Encrypt cert and forwards traffic to our hosted-UI. Auth-core resolves the incoming Host header against custom_domains, picks up the project, and serves the customer's branded panel.
Customer DNS: Cloudflare for SaaS: Authio:
auth.acme.com CNAME → custom.authio.com ──→ validates, issues ──→ edge resolves Host
LE cert, forwards → custom_domains
to origin → branding,
(preserves Host) redirect URIs,
branded senderL2 — Branded email
Sign-in / verify / recovery emails go out from noreply@acme.com instead of no-reply@authio.com. Under the hood we mint a dedicated SES email-identity for the customer's root domain, expose the three DKIM CNAMEs for them to publish, and flip the sender once SES marks DKIM as verified.
Two SES IAM users are involved:
- authio-ses-sender — least-privilege scope to
arn:aws:ses:us-east-1:*:identity/authio.com. Sends everything for platform-default tenants. Unchanged from before. - authio-ses-branded — broader scope to
arn:aws:ses:us-east-1:*:identity/*. Sends from any verified customer identity. Used exclusively for per-customer branded sends; auth-core'sBrandedwrapper routes by from-address.
L3 — Redirect URIs
Authio will only 302 a user's browser to a post-sign-in URL that's on the project's allow-list. Add yours via /settings/security; auth-core's cache invalidates within a fraction of a second of the write.
This replaces the legacy AUTHIO_DASHBOARD_REDIRECT_HOSTSenv var, which was the only way to allow a customer's callback URL until 0036 shipped. The env var still applies as a fallback for one rollout cycle.
Plan requirement: L1 and L2 are gated to the enterprise plan. L3 (redirect URIs) is available to every plan since every tenant needs at least one sign-in callback to wire their app to Authio.
Operational gotchas
Cloudflare for SaaS must be on the platform zone
If the authio.com Cloudflare zone doesn't have SSL for SaaS provisioned, the cert-issuance pipeline can't run. The customer's row will sit at cert_status = not_attempted and the dashboard surfaces “CF SaaS pending”. DNS verification still works via the legacy TXT+CNAME path; the routing CNAME is honoured. Once ops enables SSL for SaaS, every existing row picks up cert issuance on the next verify.
Per-customer SES identity verification can take 24+ hours
SES's DKIM verification is sensitive to DNS propagation. Have the customer publish the three CNAMEs, then click “Verify DKIM” in the dashboard every few minutes. Until the DKIM status flips to SUCCESS, branded email rows stay at pending_dkim and auth-core keeps sending from the platform default.
The from-address must be on the same root domain
The management-api validator rejects a from-address whose domain doesn't share a root with the registered custom hostname — e.g. auth.acme.com + noreply@acme.com is fine, auth.acme.com + noreply@notacme.com is not. This is a defense against a customer trying to send from someone else's domain.
Data model
See migration 0036_custom_domain_v2.sql for the schema. New columns on custom_domains:
cf_hostname_id textcert_status textcert_validation_records jsonblast_verified_at timestamptzbranded_email_status textbranded_email_dkim_records jsonbbranded_email_from textbranded_email_enabled_at timestamptzbranded_email_last_check_at timestamptz
And the new project_redirect_uris table for L3.
