Back to BlogArchitecture & Engineering

Custom Domains for SaaS Customers: DNS, TLS and Routing

Rupak Amin

Founder & Lead Engineer, RAITHub

12 min read

To give SaaS customers their own domain, have each customer point a CNAME (or an A record for a root domain) at your platform, verify they own it with a TXT record, let a managed service issue the TLS certificate automatically, and map the incoming Host header to a tenant in one cached lookup. Cloudflare for SaaS includes 100 custom hostnames, then charges $0.10 each.

If you would rather have custom domains built into your product for you, see how RAITHub would build this below.

The price is from Cloudflare's Cloudflare for SaaS plans page, which also caps the Free, Pro and Business plans at 50,000 hostnames. Serving the domain is the inexpensive part. Verification, routing, cookies and clean removal are where the engineering goes.

Subdomain or custom domain: what should a SaaS offer first?

Offer a subdomain of your own domain to every tenant from day one, and add customer-owned domains when customers ask for them, usually on a higher plan. The two use different mechanics.

OptionExampleDNS work for the customerTLSTypical use
Pathapp.yourapp.com/acmeNoneYour one certificateInternal tools, early MVPs
Subdomain of yoursacme.yourapp.comNoneOne wildcard certificate for *.yourapp.comDefault for every tenant
Customer subdomainportal.acme.comOne CNAME recordOne certificate per hostname, issued automaticallyCustomer portals, white-label apps
Customer root domainacme.comAn A record, or ALIAS or CNAME flattening at their DNS providerOne certificate per hostnameWebsites and storefronts built on your platform

Vercel's multi-tenant domain docs note that one wildcard certificate covers every tenant subdomain at one level, so you do not need a certificate per subdomain tenant; each customer-owned domain, by contrast, gets its own certificate. Root domains cannot use a plain CNAME, so platforms publish an IP address instead; Vercel documents an A record of 76.76.21.21 in its guide to using A records.

How does a customer connect their own domain, step by step?

  1. The customer enters the hostname in your settings screen, for example portal.acme.com. Normalise it: lower case, no scheme, no path, no trailing dot.
  2. Your app registers it with the platform (the Vercel domains API, the Cloudflare custom hostnames API, or your own allow-list) and stores it as pending.
  3. You show the exact DNS records to add: a CNAME to a target you control, such as customers.yourapp.com, and a TXT record with a random token to prove ownership.
  4. You poll for verification in the background, with a "check now" button, because DNS changes can take time to propagate. Vercel's docs warn changes can take 24–48 hours.
  5. The certificate is issued once the hostname resolves to you and validation passes. On Cloudflare for SaaS, the getting-started guide treats a hostname as ready only when both its status and its SSL status are active.
  6. You mark it active and, if the customer wants, make it the primary domain so the subdomain redirects to it.

A Cloudflare setup also needs a fallback origin, a proxied DNS record that receives all custom hostname traffic, and optionally a friendly CNAME target for customers. Vercel adds domains through its SDK with a call such as projectsAddProjectDomain, and asks for a TXT record when a domain is already in use on another Vercel account.

How should custom domains be stored and routed in code?

Keep one table that maps hostnames to tenants, and resolve the tenant from the Host header on every request, with a short cache.

CREATE TABLE tenant_domains (
  hostname           text PRIMARY KEY CHECK (hostname = lower(hostname)),
  tenant_id          uuid NOT NULL REFERENCES tenants(id),
  verification_token text NOT NULL,             -- value the customer puts in TXT
  status             text NOT NULL DEFAULT 'pending'
                     CHECK (status IN ('pending', 'verified', 'active', 'removed')),
  is_primary         boolean NOT NULL DEFAULT false,
  verified_at        timestamptz,
  created_at         timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX one_primary_domain ON tenant_domains (tenant_id) WHERE is_primary;

In Next.js 16 the request hook is proxy.ts (it was middleware.ts before; the change is explained in Next.js proxy.ts not running). A minimal host-to-tenant rewrite:

// proxy.ts
import { NextResponse, type NextRequest } from 'next/server'
import { findActiveDomain } from './lib/domains' // SELECT tenant slug WHERE hostname = $1 AND status = 'active'

const ROOT_DOMAIN = 'yourapp.com'
const cache = new Map<string, { slug: string | null; at: number }>()
const TTL_MS = 30_000

async function tenantFor(host: string): Promise<string | null> {
  if (host.endsWith('.' + ROOT_DOMAIN)) return host.slice(0, -(ROOT_DOMAIN.length + 1))
  const hit = cache.get(host)
  if (hit && Date.now() - hit.at < TTL_MS) return hit.slug
  const slug = await findActiveDomain(host)
  cache.set(host, { slug, at: Date.now() })
  return slug
}

export async function proxy(req: NextRequest) {
  const host = (req.headers.get('host') ?? '').split(':')[0].toLowerCase()
  if (host === ROOT_DOMAIN || host === 'www.' + ROOT_DOMAIN) return NextResponse.next()
  const slug = await tenantFor(host)
  if (!slug) return new NextResponse('Unknown site', { status: 404 })
  const url = req.nextUrl.clone()
  url.pathname = '/sites/' + slug + url.pathname
  return NextResponse.rewrite(url)
}

Two rules make this safe. First, the tenant comes only from the verified hostname table, never from a header or query string the client controls. Second, the tenant ID resolved here must still be enforced in the data layer, ideally with row-level security; routing picks the site, but it is not an authorization check. The isolation side is in the Postgres row-level security guide.

Can you issue TLS certificates yourself instead of using a platform?

Yes. A self-hosted reverse proxy such as Caddy can obtain certificates from a certificate authority at the moment a new hostname first connects. The Caddy automatic HTTPS docs call this On-Demand TLS and say that "to prevent abuse of this feature, you must configure restrictions": an ask endpoint that Caddy calls to check whether it may get a certificate for that domain.

// GET /internal/tls-ask?domain=portal.acme.com  (reachable only by your proxy)
export async function GET(req: Request) {
  const domain = new URL(req.url).searchParams.get('domain')?.toLowerCase()
  if (!domain) return new Response('missing domain', { status: 400 })
  const row = await findDomain(domain) // from tenant_domains
  const allowed = row !== null && (row.status === 'verified' || row.status === 'active')
  return new Response(null, { status: allowed ? 200 : 403 })
}

Without that check, anyone could point a domain at your server and make you request certificates for it, which also eats into certificate authority limits. Let's Encrypt's rate limits include 50 new certificates per registered domain every 7 days, 300 new orders per account every 3 hours, and 5 failed authorisations per identifier per account per hour. Self-hosting gives you control and no per-hostname fee, but you now run the proxy, store the certificates and watch renewals.

What are the security risks of custom domains?

  • Claiming someone else's domain. Never activate a hostname until ownership is proved by a TXT token or by traffic validation. Otherwise one tenant can attach a domain another tenant is mid-way through setting up.
  • Dangling DNS after cancellation. MDN's page on subdomain takeover describes a CNAME that still points at a service no one is serving content from. When a tenant leaves, remove the hostname from your platform, and tell the customer to delete their DNS record. When you deprovision your own hosts, MDN advises removing DNS records first.
  • Cookies leaking across tenant subdomains. Vercel's docs point out that without a Public Suffix List entry, browsers treat tenant1.yourapp.com and tenant2.yourapp.com as the same site, so one tenant can set a cookie for the whole domain. Set session cookies without a Domain attribute, use the __Host- prefix with Secure and Path=/, and keep your dashboard and login on a separate domain where possible.
  • Sign-in across domains. A session cookie set on yourapp.com is not sent to portal.acme.com. Each custom domain needs its own session, usually through a redirect-based login handoff with a one-time code, not by sharing cookies.
  • Duplicate content. If a tenant site is reachable on both the subdomain and the custom domain, redirect one to the other or set a canonical URL, as Vercel's docs suggest.

What else changes when tenants have their own domains?

Absolute links in emails and PDFs must use the tenant's primary domain, not yours. OAuth redirect URIs, CORS rules and Content-Security-Policy need to read allowed origins from the domain table, not a hard-coded list. Your status checks should probe a sample of custom hostnames, because a certificate renewal failure there will not show on your main domain. And emails sent "from" the customer's domain need SPF, DKIM and DMARC records on their side, a separate setup from web traffic. The full white-label picture, including branded email and theming, is in building a white-label SaaS platform.

Do-it-yourself estimate: 4–8 days on Vercel or Cloudflare for SaaS for the domain table, settings screen, verification polling, routing, cookie changes and tests, if your app is already multi-tenant. The main risk is the unhappy paths: domains that never verify, a removed tenant whose DNS still points at you, and a login flow that breaks on the custom domain.

Buy, build or hire?

OptionExamplesChoose this whenWatch out for
Hosting platform's domain APIVercel multi-tenant domainsYour app already runs on that platform and tenant counts fit its plan limitsCheck your plan's domain limits and the platform's rules before you promise unlimited domains
Edge custom-hostname serviceCloudflare for SaaS: 100 hostnames included, then $0.10 eachMany tenants, any origin host, and you want certificates and DDoS protection at the edgeA fallback origin and validation flow to set up; some features are Enterprise-only
Self-hosted proxyCaddy On-Demand TLS with an ask endpointYou run your own servers and want no per-hostname feeYou own certificate storage, renewals, rate limits and uptime
Hire a team to build itRAITHub or another studioDomains, auth, cookies and tenant isolation must all change togetherGet the removal and takeover tests in the handover

How do you test custom domains?

  • Ownership: try to add a domain already active for another tenant; it must be refused.
  • Lifecycle: add, verify, make primary, remove, re-add; after removal the hostname must return 404, not the old tenant's site.
  • Isolation: log in on tenant A's custom domain and check no request can read tenant B's data, even with B's IDs.
  • Cookies: confirm session cookies carry no Domain attribute and are not sent to sibling subdomains.
  • Certificates: monitor expiry dates on a sample of custom hostnames and alert well before expiry.

Why RAITHub for this

  • Multi-tenant platforms. BlockEstate is a multi-tenant listing and inquiry platform built to MVP in 6 weeks.
  • Isolation enforced in the database. Sundor Skin runs 146 PostgreSQL tables with row-level security and 88 permission codes, covered by 530+ tests.
  • Next.js in production. This site runs on Next.js with 400+ tests, and the proxy.ts migration is documented in the post linked above.

RAITHub has not published a case study specific to custom tenant domains; the guidance here is engineering practice.

When you don't need us

  • Your tenants are happy with subdomains. A wildcard domain and one rewrite are enough.
  • You have a handful of custom domains. Adding them by hand in your hosting dashboard is fine until it is not.
  • You want developers placed in your team. RAITHub does fixed-scope builds and dedicated teams, not staff augmentation.

How RAITHub would build this

  • Domain model and settings screen: add, verify, make primary and remove, with exact DNS instructions per customer.
  • Platform integration: Vercel, Cloudflare for SaaS or a self-hosted proxy with an ask endpoint, chosen for your hosting and tenant count.
  • Routing and auth: cached host-to-tenant resolution, per-domain sessions with a secure login handoff, and safe cookies.
  • Safety nets: ownership checks, clean removal to prevent takeover, and certificate expiry monitoring.

Timeline: inside a new product, part of the 4–6 week fixed-scope SaaS build; added to an existing app, it fits within the 6–12 week backend range alongside other work, with exact scope in the quote.

You receive: lifecycle and isolation tests in CI, handover docs and runbooks for domain support tickets, and full IP assigned to you under NDA.

Next step: a free 15-minute technical audit, then a written fixed quote. See the SaaS development service, or book the audit.

Frequently asked questions

How do SaaS apps support custom domains?

The customer points a DNS record at the platform, the platform verifies ownership and issues a TLS certificate automatically, and the app maps the request's Host header to the right tenant.

What DNS record should customers add for a custom domain?

A CNAME for a subdomain such as portal.acme.com, pointing at a target you provide. A root domain such as acme.com cannot use a plain CNAME, so it needs an A record or the DNS provider's ALIAS or flattening feature, plus a TXT record for ownership.

How much does Cloudflare for SaaS cost for custom domains?

Per Cloudflare's plans page, every plan includes 100 custom hostnames, and Free, Pro and Business plans charge $0.10 for each additional hostname, up to 50,000. Enterprise pricing is custom.

Do I need a separate SSL certificate for each customer domain?

Yes, each customer-owned hostname needs its own certificate, but platforms issue and renew them automatically. Subdomains of your own domain can share one wildcard certificate.

What happens to a custom domain when a customer cancels?

Remove the hostname from your platform and domain table so it stops serving, and ask the customer to delete their DNS record. Leaving either in place risks a subdomain takeover.

How do logins work across custom domains?

Cookies do not cross domains, so each custom domain needs its own session. A common pattern is to log in on a central domain and hand off to the custom domain with a short-lived, one-time code.

Custom domainsMulti-tenant SaaSTLS certificatesCloudflare for SaaSVercelDNSNext.js

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.