← All articles

Custom Domains with Automated SSL for a Multi-Tenant SaaS (Cloudflare for SaaS)

By Shuvo Haldar4 min read
SaaSMulti-TenantCloudflareSSLArchitecture
Custom Domains with Automated SSL for a Multi-Tenant SaaS (Cloudflare for SaaS)

The feature request sounded small: "let creators use their own domain." It isn't. On Krey, a creator-commerce platform I built, a storefront can live at a platform subdomain or at shop.theirbrand.com — a domain the platform has never seen before, owned by someone else, that needs a valid TLS certificate the moment they point DNS at us. Multiply that by every creator on the platform and you can't be issuing certs by hand.

I solved it with Cloudflare for SaaS, and the interesting part is how little of the problem ends up being mine.

Animated onboarding flow: creator saves a domain (POST custom_hostnames with HTTP DV), adds one CNAME, Cloudflare validates over HTTP and issues plus renews the certificate, then a webhook flips the domain from pending to active and the storefront goes live.
The creator does one thing — add a CNAME. Validation, the certificate, and the pending→active flip all follow automatically.

What actually has to happen

When a creator adds a custom domain, three things need to be true before their storefront works:

  1. They prove they control the domain (so nobody claims google.com).
  2. A TLS certificate gets issued for it and renewed forever after.
  3. Incoming requests for that hostname get routed to the right tenant.

The trap is thinking you'll do 1 and 2 yourself — build ACME challenges, store private keys, run a renewal cron, handle rate limits from the CA. That's a whole product on its own, and it's a security surface you don't want to own if you don't have to.

Hand the hard part to Cloudflare

Cloudflare for SaaS turns this into an API call plus a DNS instruction. I register the creator's hostname as a custom hostname on the platform's Cloudflare zone; Cloudflare handles domain-control validation and issues + renews the certificate. The creator just adds one CNAME.

// When a creator saves a custom domain, register it as a custom hostname.
const res = await fetch(
  `https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/custom_hostnames`,
  {
    method: "POST",
    headers: { Authorization: `Bearer ${CF_TOKEN}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      hostname: creatorDomain,
      ssl: { method: "http", type: "dv", settings: { min_tls_version: "1.2" } },
    }),
  },
);
const { result } = await res.json();
await saveDomain({ storefrontId, hostname: creatorDomain, cfId: result.id, status: "pending" });

The ssl.method: "http" is the choice that makes onboarding painless: Cloudflare validates control over HTTP once the CNAME resolves, so the creator does one thing — add the CNAME — and both verification and the certificate follow automatically. No TXT record dance, no waiting on me.

The tenant lookup is now the whole backend job

Once TLS is off my plate, my job shrinks to one question: a request arrived for shop.theirbrand.com — whose storefront is this? Every custom hostname maps to a tenant, so the incoming Host header becomes the tenant key.

Diagram: incoming requests carrying different Host headers pass through resolveStorefront, which looks up the verified domain from cache backed by Postgres and only resolves active domains, mapping each host to its tenant storefront while a pending domain shows a verifying page.
The Host header resolves to a tenant through a cached lookup — only active domains serve; pending ones get a clean verifying page.
// Resolve the tenant from the host on every storefront request.
export async function resolveStorefront(host: string) {
  const domain = await findVerifiedDomainByHostname(host); // cached, then Postgres
  if (!domain) throw new NotFoundError("No storefront for this domain");
  return getStorefront(domain.storefrontId);
}

I keep the domain-to-tenant map cached because it's read on every single request and changes rarely. The Cloudflare webhook (or a poll of the custom-hostname status) flips a domain from pending to active when the cert is live, and only active domains resolve — so a half-set-up domain shows a clean "verifying your domain" state instead of a broken TLS error.

Why I don't build this myself anymore

I've seen teams spend weeks on a homegrown ACME pipeline for exactly this feature. My honest take: unless certificate issuance is your product, don't own it. The failure modes — rate limits, renewal at 2am, a leaked private key — are all downside with no differentiation. Cloudflare for SaaS (or a comparable managed layer) turns "give every tenant their own HTTPS domain" from a subsystem into a form field plus a status column.

What stays mine is the part that's actually about my product: mapping a hostname to a tenant, gating on verification status, and rendering the right storefront. That's where the value is, and it's a page of code.

Takeaways

  • Custom-domain SSL is three problems — control validation, cert lifecycle, routing. Only routing is really yours.
  • Use a managed layer (Cloudflare for SaaS) for issuance and renewal; make the creator's job a single CNAME with HTTP validation.
  • Model a domain with an explicit pendingactive status and only resolve verified ones, so onboarding never surfaces a scary cert error.
  • Treat the Host header as the tenant key and cache the lookup — it runs on every request.

The best infrastructure code is the code you talked yourself out of writing.


I build multi-tenant SaaS platforms end-to-end. If you're working on something similar, let's talk.

Building something similar?

I'm a senior full-stack & DevOps engineer available for full-time roles and projects worldwide.