Adding custom domains to a Cloudflare Workers app is mostly a question of who issues the SSL certificate. The Worker itself can route by hostname in a couple of lines — but Workers do not mint certificates for domains your customers own. You either use Cloudflare for SaaS Custom Hostnames, or put a provider-independent proxy in front. Here are both paths, honestly.
// worker.ts — route by the hostname the visitor used
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const host = new URL(request.url).hostname;
if (host === 'yourapp.com' || host.endsWith('.yourapp.com')) {
return handleOwnDomain(request, env);
}
const tenant = await env.TENANTS.get(host, 'json'); // KV lookup
if (!tenant) return new Response('Unknown domain', { status: 404 }); // fail closed
return handleTenant(request, env, tenant);
},
};The SSL decision (this is the whole post)
Option A — Cloudflare for SaaS (Custom Hostnames)
Cloudflare issues and renews a certificate for each customer hostname and routes it to your zone / Worker. You call their API to add a custom hostname:
curl -X POST \
"https://api.cloudflare.com/client/v4/zones/$ZONE/custom_hostnames" \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
-d '{"hostname":"app.customer.com","ssl":{"method":"http","type":"dv"}}'The customer CNAMEs app.customer.com to your SaaS fallback origin, Cloudflare validates and issues, and your Worker sees the hostname. It works well — if you are happy being all-in on Cloudflare and with per-hostname pricing. We lay out when that is and is not the right call in the Cloudflare for SaaS alternative comparison.
Option B — A provider-independent proxy in front of the Worker
If you want the domain + SSL layer decoupled from Cloudflare (multi-CDN, portability, your own header/redirect policies), terminate TLS on an independent edge and proxy to your Worker. Your Worker code above does not change — it still routes by host.
Routing and failing closed
However you provision, the Worker resolves the hostname to a tenant from KV, D1, or your API and 404s unknown hosts. A permissive default here is a cross-tenant data leak, not a cosmetic bug. Keep the lookup in KV or cache it — it is on every request.
The one-API-call way
cnames.dev gives you the Option B edge without building it: register a hostname pointed at your Worker (or any origin) and SSL, routing, and real client IP are handled, without tying your domain layer to a single CDN.
curl -X POST https://api.cnames.dev/v1/domains \
-H "Authorization: Bearer sk_live_..." \
-d '{"hostname":"app.customer.com","origin":"your-worker.example.workers.dev"}'Verify the customer's record with the CNAME checker, and read the full architecture in the complete guide.