How to set up custom domains for your SaaS with Cloudflare SSL for SaaS (and a simpler way)
Letting your users connect their own domain to your SaaS is one of those features that feels like a weekend project. It isn't. You need TLS termination on an edge that can recognize thousands of hostnames, automated certificate issuance, a renewal cron that runs forever, DNS health checks, and a way for the customer to actually wire it up at their registrar.
Cloudflare SSL for SaaS is the long-standing answer at scale. It runs JetBlue, Olo, Indeed. It works.
But before you commit a sprint to it, walk through what the integration actually involves and where the sharp edges sit. After that you can decide between Cloudflare's depth-and-complexity path or Domainee's 5-minute path with a real free tier.
This post: an honest tutorial for both. Cloudflare first, Domainee second. Side-by-side at the end.
Where Cloudflare SSL for SaaS gets complicated
The good news first: Cloudflare's hostname price is competitive. 100 custom hostnames are included on Free, Pro, and Business plans. After that, $0.10 per hostname per month, up to a 50,000-hostname PAYG cap (raised from 5,000 in May 2025). Above that you talk to sales.
The complications:
Three SSL validation flows, and you pick
- HTTP DCV — easiest to set up but doesn't work for wildcards and has a race condition where DNS can cut over before the cert is issued.
- TXT DCV — bulletproof. Customer adds a TXT record at
_acme-challenge.shop.acme.com. For wildcards you need two TXT tokens (apex + wildcard). - Delegated DCV — one-time CNAME delegation, lets Cloudflare handle renewals forever without bothering the customer again. Conflicts with multi-CDN setups (only one party can hold the delegation).
You'll spend support cycles explaining the trade-offs to customers.
Rate limit you can hit during onboarding spikes
- 15 certificates per minute by default. Exceed it and you get a 30-second lockout. Higher limits require an account-manager request.
- Hostnames over 64 characters require setting
cloudflare_branding: truein the API because of certificate Common Name restrictions.
Two-step setup before you can issue anything
Before the per-customer API calls work, you have to:
- Enable Custom Hostnames on your zone via the Cloudflare dashboard (SSL/TLS → Custom Hostnames).
- Configure a fallback origin — a proxied A or CNAME record on your zone, designated as the fallback. Status must read Active.
- (Optional) Pick a CNAME target like
customers.yoursaas.comso customers point their CNAME at a friendly name instead of your apex. - (Optional) Add a Worker as the origin if you want per-tenant routing logic. Required for Workers-for-Platforms dispatch patterns.
Webhooks are Enterprise-only
On Free, Pro, and Business plans you have to poll the custom-hostname status. Both result.status AND result.ssl.status need to read "active" before traffic works. Hostname webhooks ship on Enterprise.
Cloudflare Enterprise is required for
- Custom metadata per hostname (useful for tenant-mapping at the edge)
- BYOIP (your own anycast IPs)
- Apex proxying
- Webhooks on hostname state changes
Enterprise pricing isn't published. Onboarding involves a sales call.
Error 1016 will be your most-debugged ticket
"Origin DNS error" hits when:
- Fallback origin DNS record missing or not designated
- Ownership validation hasn't completed
- Customer's CAA records at their DNS block your CA (Let's Encrypt / Google Trust Services / SSL.com)
- Customer's zone has a hold that the previous owner hasn't released
None of these are obvious to the customer. You write the runbook.
The Domainee alternative, in one paragraph
50 custom domains and 100 GB of bandwidth a month, free forever, no card. After that, $0.20/domain/month graduated to $0.10 at 10,000+ domains. One CNAME for each customer. SSL provisions on first request, renewals run forever. Webhooks fire on every plan. The integration is one API call per customer plus one webhook handler. No fallback origin to configure, no validation flow to choose between, no 1016 debugging.
If you don't already run Cloudflare's WAF and DDoS shield, the math and the developer experience both favor Domainee in the 1 to ~5,000 hostnames band.
OK. Tutorials.
Tutorial 1: Cloudflare SSL for SaaS
Prerequisites:
- A Cloudflare account with your SaaS root domain on at least the Free plan.
- An API token with the Custom Hostnames Edit permission. Mint it at My Profile → API Tokens.
- Your origin server reachable on a public hostname.
Step 1. Enable Custom Hostnames on your zone
In the dashboard: select your zone, go to SSL/TLS → Custom Hostnames, toggle on.
Step 2. Configure the fallback origin
Add a proxied A or CNAME record on your zone that points to your origin server (e.g. origin.yoursaas.com → 203.0.113.50). Save.
Back in Custom Hostnames, click Add Fallback Origin, pick the record. Wait for status to flip to Active.
Step 3. (Optional) Create a friendly CNAME target
Customers don't want to CNAME to yoursaas.com. Create a wildcard:
*.customers.yoursaas.com CNAME yoursaas.com
This is the CNAME target you'll give your customers.
Step 4. Provision a custom hostname per customer (API)
Every time a customer adds a domain in your app's dashboard:
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/custom_hostnames" \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"hostname": "shop.acme.com",
"ssl": {
"method": "http",
"type": "dv"
}
}'
The response gives you the hostname id, the SSL validation tokens, and the current status.
Step 5. Tell the customer the CNAME
Type Name Value
CNAME shop.acme.com customers.yoursaas.com
Note: if the customer's DNS provider has restrictive CAA records (limiting which CAs can issue), they may need to add a CAA exception for letsencrypt.org or google.com or sectigo.com (Cloudflare picks the CA).
Step 6. Poll until cert is live
Both fields must read "active":
curl "https://api.cloudflare.com/client/v4/zones/{zone_id}/custom_hostnames/{hostname_id}" \
-H "Authorization: Bearer $CF_TOKEN"
# Expected response when ready:
# {
# "result": {
# "id": "...",
# "hostname": "shop.acme.com",
# "status": "active",
# "ssl": { "status": "active" }
# }
# }
Loop every 30 seconds for the first 5 minutes. Most domains land in under 90 seconds, some take longer due to DNS propagation.
Step 7. (Optional) Delegated DCV for hands-off renewals
If you want renewals to never bother the customer again, ask them to set:
Type Name Value
CNAME _acme-challenge.shop.acme.com shop.acme.com.YOURZONE.dcv.cloudflare.com
Cloudflare will handle all future renewal validations via DNS-01 without re-prompting.
Step 8. Handle errors as they come up
Common failure modes you'll see in support:
- Error 1016 — fallback origin not set, or hostname not pointing at the right zone
- TLS handshake error — DNS cut over before cert issued (HTTP DCV race)
- CAA blocks issuance — customer's DNS has a strict CAA record
- CNAME chain too long — customer pointed www.shop.acme.com to shop.acme.com to your CNAME target. Two-hop chains can fail; recommend a direct CNAME.
Plan a few hours of support reading for your first 10-20 onboardings.
Tutorial 2: Domainee
Step 1. Mint an API key
Sign up at /sign-up. From /developers click Create API key. Copy the sk_live_… string. It's shown once.
Step 2. One API call per customer
Run this from your app's backend when a customer adds a domain:
curl https://api.domainee.dev/v1/domains \
-H "Authorization: Bearer $DOMAINEE_KEY" \
-H "Content-Type: application/json" \
-d '{
"hostname": "shop.acme.com",
"originUrl": "https://acme-prod.fly.dev",
"metadata": { "tenantId": "tnt_77721" }
}'
Response includes the CNAME for the customer:
{
"domain": {
"id": "8f09b47c-…",
"status": "pending",
"dnsRecords": [
{ "type": "CNAME",
"name": "shop.acme.com",
"value": "edge.domainee.dev" }
]
}
}
Step 3. Tell the customer the CNAME
Type Name Value
CNAME shop.acme.com edge.domainee.dev
That's it on the customer side.
Step 4. Receive the webhook when DNS lands
Domainee fires domain.verified the moment DNS resolves correctly and the cert provisions. HMAC-signed, retried for 7 days if your endpoint is down.
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post(
"/webhooks/domainee",
express.raw({ type: "application/json" }),
async (req, res) => {
const sig = req.header("x-domainee-signature");
const expected = "sha256=" +
crypto.createHmac("sha256", process.env.WHSEC)
.update(req.body)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString());
if (event.type === "domain.verified") {
await db.tenants.update(event.data.metadata.tenantId, {
customDomainStatus: "live",
});
}
res.status(200).end();
},
);
Step 5. There isn't one
No fallback origin to configure. No validation flow to pick. No polling loop. SSL renewals run forever. DNS monitoring is built in and fires domain.monitor_updated if anything drifts.
If your AI tooling (Cursor, Claude Code, Claude Desktop) should drive Domainee too, drop the MCP server config into your client and the same Bearer key works for natural-language operations.
Side-by-side, after both tutorials
| Cloudflare SSL for SaaS | Domainee | |
|---|---|---|
| Free quota | 100 hostnames included | 50 hostnames + 100 GB bw, no card |
| Per-hostname price | $0.10/mo | $0.20/mo → $0.10 at 10k+ |
| Setup steps before first customer | 4 (zone enable, fallback origin, CNAME target, API token) | 1 (mint API key) |
| Setup steps per customer | 2 (provision API call + poll) | 1 (one API call) |
| Validation methods you choose between | 3 (HTTP, TXT, Delegated DCV) | 0 (we pick) |
| Webhooks | Enterprise only | All plans |
| MCP / AI agent | No | Yes |
| Per-customer origin URL | One fallback (Worker for per-tenant routing) | Per hostname in the API call |
| Hostname cap | 50,000 (PAYG) | Unlimited (price graduates) |
| Enterprise pricing | Not published | Published, graduates to $0.10 at 10k+ |
| Time from signup to first verified domain | ~30 minutes | ~5 minutes |
When Cloudflare is actually the right pick
Be honest about this. Cloudflare wins when:
- You already run Cloudflare for WAF, DDoS, Workers, or KV
- You need Cloudflare-platform features (Workers for Platforms, custom metadata per hostname, BYOIP)
- You have the engineering bandwidth to maintain validation logic, fallback origins, and 1016 debugging
- You're at 10,000+ hostnames where the per-hostname math wins on $0.10 flat versus Domainee's graduated curve
- You're already shopping for an Enterprise contract for the rest of Cloudflare's stack
When Domainee is the right pick
- You want to ship custom domains today, not after a 2-week setup
- 50 free hostnames + no card lets you validate the feature before paying anyone
- You want webhooks on every plan
- You want AI tooling to drive it via MCP
- Your scale is somewhere in the 1 to 10,000 hostnames band
- You don't want to choose between three SSL validation flows
Try Domainee
Mint an API key. 50 customer domains free, no card. The first customer's domain can go live in the time it takes to read this paragraph.
If you want the deep-dive on the Connect API integration: docs/quickstart. For MCP setup: docs/mcp. For webhooks: docs/webhooks.