Custom Domains for SaaS: Wildcard Subdomains & Automated SSL
Your customer types app.theircustomer.com into their browser and waits. Behind that three-second load, your infrastructure is orchestrating a DNS lookup, validating a wildcard certificate, routing through your reverse proxy, and serving their branded dashboard -- all without them knowing your actual server IP. I've shipped this custom domain pattern four times across different stacks, and every implementation surfaced gotchas that never appeared in the product spec: Let's Encrypt rate limits that block your tenth customer onboarding, CNAME flattening behavior that breaks on specific DNS providers, propagation delays that make support tickets pile up during launch week. You'll architect this once thinking it's a two-sprint project -- before you discover the edge cases that turn SSL automation into a weekend of Cloudflare API debugging and certificate chain validation.
This article is the playbook I wish I'd had. We're going to cover wildcard subdomains for multi-tenant apps, automated SSL provisioning with Let's Encrypt and alternatives, custom vanity domains where customers bring their own, and the operational concerns that'll bite you at 2 AM if you don't plan for them.
Table of Contents
- Why Custom Domains Matter for SaaS
- Architecture Overview: Three Approaches
- Wildcard Subdomains for Multi-Tenancy
- Automated SSL with Let's Encrypt
- Custom Vanity Domains: The Full Flow
- Infrastructure Patterns by Platform
- DNS Configuration and Verification
- Production Hardening and Monitoring
- Cost Analysis at Scale
- FAQ

Why Custom Domains Matter for SaaS
Custom domains aren't just a vanity feature. They're a trust signal. When your customer's client visits portal.acmecorp.com instead of acmecorp.your-saas.io, it reinforces the customer's brand. It removes friction. And for many enterprise deals, it's a hard requirement -- the procurement team won't approve software that forces employees onto a third-party domain.
There's also an SEO angle. If you're building a SaaS that involves public-facing pages (think Shopify stores, landing page builders, knowledge bases, or client portals), customers need their own domains to build domain authority. You can't do that on a subdomain of someone else's platform. Well, you can, but it's suboptimal.
The three most common patterns I see:
- Platform subdomains --
customer.your-app.com(easiest) - Custom subdomains --
app.customer.com(medium complexity) - Apex custom domains --
customer.com(hardest, because CNAME records don't work at the apex)
Most mature SaaS products end up supporting all three.
Architecture Overview: Three Approaches
Before we get into implementation details, let's look at the three main architectural approaches and when each makes sense.
| Approach | Complexity | Best For | SSL Method | DNS Requirement |
|---|---|---|---|---|
| Wildcard subdomain | Low | Platform-controlled subdomains | Single wildcard cert | Wildcard DNS A/AAAA record |
| Reverse proxy with SNI | Medium | Custom domains at moderate scale | Per-domain cert via ACME | Customer CNAME or A record |
| CDN/Edge with custom domains | Low-Medium | High scale, global distribution | Managed by CDN provider | Customer CNAME or A record |
| Dedicated load balancer per tenant | High | Enterprise isolation requirements | Per-tenant cert | Customer DNS delegation |
For most SaaS applications, you'll start with wildcard subdomains and eventually add reverse-proxy-based custom domain support. Let's dig into each.
Wildcard Subdomains for Multi-Tenancy
This is your starting point. Every tenant gets {slug}.yourapp.com. Here's how to set it up properly.
DNS Configuration
You need a wildcard DNS record:
*.yourapp.com. 300 IN A 203.0.113.10
*.yourapp.com. 300 IN AAAA 2001:db8::1
This means any subdomain of yourapp.com resolves to your server. Your application then reads the Host header to determine which tenant to serve.
Application-Level Routing
In a Next.js app (which we build a lot of at Social Animal), you'd handle this in middleware:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const hostname = request.headers.get('host') || '';
const subdomain = hostname.split('.')[0];
// Skip for main domain and known subdomains
if (['www', 'app', 'api'].includes(subdomain)) {
return NextResponse.next();
}
// Rewrite to tenant-specific path
const url = request.nextUrl.clone();
url.pathname = `/tenant/${subdomain}${url.pathname}`;
return NextResponse.rewrite(url);
}
export const config = {
matcher: ['/((?!_next|api|static).*)'],
};
For Astro-based sites (another framework we use heavily), you'd handle this in your server middleware or at the edge.
Wildcard SSL Certificate
A single wildcard certificate covers all subdomains:
certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d 'yourapp.com' \
-d '*.yourapp.com'
Note: wildcard certificates from Let's Encrypt require DNS-01 challenge validation. You can't use HTTP-01 for wildcards. This means you need API access to your DNS provider. Certbot has plugins for Cloudflare, Route53, Google Cloud DNS, and most major providers.
Let's Encrypt wildcard certs are valid for 90 days, so automate that renewal. Seriously. Set a monitoring alert if renewal fails -- don't be the person who takes down 500 customer sites because a cert expired.

Automated SSL with Let's Encrypt
When customers bring their own domains, you need per-domain certificates. This is where things get interesting.
The ACME Protocol
Let's Encrypt uses the ACME (Automatic Certificate Management Environment) protocol. The two challenges you'll care about:
- HTTP-01: You prove domain ownership by serving a specific file at
http://yourdomain.com/.well-known/acme-challenge/{token}. Easiest to automate, works for individual domains. - DNS-01: You prove ownership by creating a specific TXT record. Required for wildcards, harder to automate for customer domains (you don't control their DNS).
For custom domains, you'll almost always use HTTP-01. The flow looks like this:
- Customer adds a CNAME pointing their domain to your platform
- Your system detects the DNS is resolving correctly
- You initiate an ACME certificate request
- Let's Encrypt sends an HTTP-01 challenge
- Your server responds with the correct challenge token
- Certificate is issued and stored
- Your reverse proxy starts serving HTTPS for that domain
Caddy: The Lazy (Smart) Option
Honestly, if you're not already committed to nginx, just use Caddy. It handles automatic HTTPS out of the box, including on-demand TLS for unknown domains:
{
on_demand_tls {
ask http://localhost:5555/check-domain
interval 2m
burst 5
}
}
https:// {
tls {
on_demand
}
reverse_proxy localhost:3000
}
The ask endpoint is critical -- it's where Caddy checks with your application whether a domain is actually a valid customer domain before requesting a certificate. Without this, anyone could point their domain at your IP and trigger certificate requests, potentially burning through Let's Encrypt rate limits.
// /check-domain endpoint
app.get('/check-domain', async (req, res) => {
const domain = req.query.domain;
const isValid = await db.customDomains.findOne({
domain,
verified: true,
active: true
});
if (isValid) {
res.status(200).send('OK');
} else {
res.status(404).send('Not found');
}
});
Nginx + Certbot Approach
If you're already running nginx, you can use certbot with the webroot plugin:
certbot certonly --webroot -w /var/www/certbot \
-d customer-domain.com \
--non-interactive --agree-tos \
--email ssl@yourapp.com
You'll need to dynamically update nginx configs and reload:
server {
listen 443 ssl;
server_name customer-domain.com;
ssl_certificate /etc/letsencrypt/live/customer-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/customer-domain.com/privkey.pem;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
This works but it's painful to manage at scale. Every new domain means generating a config, requesting a cert, and reloading nginx. You'll want to build automation around this, and honestly Caddy's on-demand TLS is just easier.
Let's Encrypt Rate Limits (2025)
These matter and you need to plan around them:
| Limit | Value | Notes |
|---|---|---|
| Certificates per Registered Domain | 50/week | Per your root domain |
| Duplicate Certificate | 5/week | Same exact set of hostnames |
| Failed Validations | 5/hour per account per hostname | Can block you fast |
| New Orders | 300/3 hours | Account-wide |
| Pending Authorizations | 300/account | Clean up old ones |
At 50 certs per week, if you're onboarding more than ~7 custom domains per day, you need to think about this. Options:
- Use a different ACME CA (ZeroSSL, BuyPass) as fallback
- Apply for a Let's Encrypt rate limit increase (they grant these for legitimate SaaS use cases)
- Pre-validate DNS before attempting certificate issuance
- Implement retry logic with exponential backoff
Custom Vanity Domains: The Full Flow
Here's the complete user journey I recommend, having built this flow multiple times.
Step 1: Customer Enters Domain in Your Dashboard
Provide clear instructions. Show them exactly what DNS record to create:
Type: CNAME
Host: portal (or whatever subdomain they want)
Value: custom.yourapp.com
TTL: 300
For apex domains (bare customer.com), they need an A record pointing to your IP, or they need a DNS provider that supports CNAME flattening (Cloudflare, Route53 ALIAS records, etc.).
Step 2: DNS Verification
Don't just check once. DNS propagation can take minutes to hours. Implement a polling mechanism:
async function verifyDomain(domain: string, expectedTarget: string): Promise<boolean> {
try {
const records = await dns.promises.resolveCname(domain);
return records.some(r => r === expectedTarget);
} catch (err) {
// For A records (apex domains)
try {
const aRecords = await dns.promises.resolve4(domain);
return aRecords.some(r => EXPECTED_IPS.includes(r));
} catch {
return false;
}
}
}
// Poll every 30 seconds for up to 24 hours
async function waitForDNS(domain: string) {
const maxAttempts = 2880; // 24 hours at 30s intervals
for (let i = 0; i < maxAttempts; i++) {
if (await verifyDomain(domain, 'custom.yourapp.com')) {
return true;
}
await sleep(30000);
}
return false;
}
Step 3: Certificate Provisioning
Once DNS is verified, request the certificate. With Caddy, this happens automatically on first request. With nginx/certbot, trigger it programmatically.
Step 4: Ongoing Monitoring
Domains can break. Customers change DNS records accidentally. Certificates expire if renewal fails. You need to monitor:
- DNS resolution still points to you (check daily)
- Certificate expiry dates (alert at 14 days, critical at 7)
- SSL handshake success (synthetic monitoring)
- HTTP response codes on customer domains
Infrastructure Patterns by Platform
The approach varies significantly depending on where you're hosting.
Vercel
Vercel has built-in custom domain support. For a multi-tenant Next.js app, you'd use their Domains API:
curl -X POST "https://api.vercel.com/v10/projects/{projectId}/domains" \
-H "Authorization: Bearer $VERCEL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "portal.customer.com"}'
Vercel handles SSL automatically. The main limitation: you're subject to their pricing. At the Pro plan ($20/team member/month), you get 50 domains per project. Enterprise gives you more. If you have thousands of customers with custom domains, the cost adds up.
Cloudflare for SaaS (SSL for SaaS)
This is probably the best option for scale in 2025. Cloudflare's SSL for SaaS product (previously called Custom Hostnames) handles everything:
- Automatic certificate issuance and renewal
- Global CDN and DDoS protection
- Built-in domain verification
- API for programmatic management
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": "portal.customer.com",
"ssl": {
"method": "http",
"type": "dv"
}
}'
Pricing: $0.10/custom hostname/month after the first 100 (which are free on the Enterprise plan). For a SaaS with 1,000 custom domains, that's about $90/month. Very reasonable.
AWS (ALB + ACM + Route53)
If you're on AWS, you can use:
- ALB for routing with SNI-based certificate selection
- ACM (AWS Certificate Manager) for free certificates
- Route53 for DNS verification
The catch: ACM certificates are only usable with AWS services (ALB, CloudFront, API Gateway). And ALB has a limit of 25 certificates per load balancer by default (can be increased to 100). For serious scale, you'd put CloudFront in front with its 100 certificate limit per distribution.
This gets expensive and complex. I'd honestly recommend Cloudflare for SaaS over this approach unless you have specific AWS requirements.
Fly.io
Fly.io has a nice fly certs add command and API for adding custom domains:
fly certs add portal.customer.com
It handles Let's Encrypt automatically. Works well for small to medium scale.
DNS Configuration and Verification
The DNS piece trips up more teams than any other part of this feature. Here's what you need to know.
The CNAME-at-Apex Problem
Customers will inevitably want to use their bare domain (customer.com, no subdomain). The DNS spec doesn't allow CNAME records at the apex of a zone because CNAME records must be exclusive -- they can't coexist with other record types, and the apex always has SOA and NS records.
Solutions:
- CNAME flattening (Cloudflare) -- resolves the CNAME at the DNS level, returns an A record
- ALIAS records (Route53, DNSimple) -- proprietary record type that works like a CNAME at the apex
- ANAME records (some providers) -- similar to ALIAS
- A record -- just give customers your IP address
Option 4 (A records) is the most universal but the least flexible. If you ever change your server's IP, every customer using an A record needs to update their DNS. With a CNAME, you just update what your CNAME target resolves to.
My recommendation: support both. Tell customers to use a CNAME for subdomains and an A record (or ALIAS/ANAME if their provider supports it) for apex domains.
Ownership Verification
Beyond confirming the domain resolves to your infrastructure, you might want to verify that the person configuring the domain in your SaaS actually controls it. A common approach is requiring a TXT record:
_verification.customer.com TXT "yourapp-verify=abc123unique"
This prevents someone from pointing a domain they don't own at your platform and claiming it belongs to their account.
Production Hardening and Monitoring
This section is the difference between a demo and a production system.
Certificate Storage
Don't store certificates on disk if you're running multiple instances. Use a distributed store:
- Caddy: supports storage backends for Redis, Consul, S3, PostgreSQL
- Custom: store certs in an encrypted database or secrets manager (AWS Secrets Manager, HashiCorp Vault)
Graceful Fallback
What happens when a customer's domain is misconfigured? Don't show an SSL error. Instead:
- Detect the failed SSL handshake
- Redirect to a status page explaining the issue
- Send a notification to the customer
- Automatically retry certificate provisioning
Health Checks
Build a background job that regularly checks every custom domain:
async function healthCheckDomains() {
const domains = await db.customDomains.find({ active: true });
for (const domain of domains) {
const checks = {
dnsResolves: await checkDNS(domain.hostname),
sslValid: await checkSSL(domain.hostname),
httpOk: await checkHTTP(domain.hostname),
};
if (!checks.dnsResolves) {
await alertCustomer(domain, 'DNS no longer points to our platform');
await markDomain(domain, 'dns_error');
} else if (!checks.sslValid) {
await triggerCertRenewal(domain);
}
await db.domainHealthChecks.insert({
domainId: domain.id,
...checks,
checkedAt: new Date(),
});
}
}
Run this every hour. At 10,000 domains, the checks complete in a few minutes if you parallelize properly.
Cost Analysis at Scale
Let's talk real numbers for 2025 pricing.
| Solution | 100 Domains | 1,000 Domains | 10,000 Domains | Notes |
|---|---|---|---|---|
| Caddy + Let's Encrypt (self-managed) | ~$50/mo server | ~$200/mo servers | ~$1,000/mo servers | Your ops burden |
| Cloudflare for SaaS | Free (Enterprise) | ~$90/mo | ~$990/mo | Best value at scale |
| Vercel (Pro) | Included | $0 extra | Need Enterprise | Limited to 50/project on Pro |
| AWS CloudFront + ACM | ~$100/mo | ~$300/mo | ~$2,000/mo | Includes CDN transfer costs |
| Fastly | ~$150/mo | ~$500/mo | Custom pricing | Good if already using Fastly |
For most SaaS products, Cloudflare for SaaS hits the sweet spot. You get global CDN, DDoS protection, and automated certificates for about a dime per domain per month. Hard to beat that.
If you're building on a headless CMS architecture -- something we do a lot -- Cloudflare for SaaS pairs particularly well since you're already dealing with decoupled frontends that benefit from edge caching.
FAQ
How long does it take for a custom domain to become active after DNS configuration?
DNS propagation typically takes 5-30 minutes, though it can take up to 48 hours in rare cases. Certificate provisioning with Let's Encrypt adds another 30-60 seconds once DNS is resolving correctly. In practice, most custom domains are fully active within 15 minutes. I recommend polling DNS every 30 seconds and triggering certificate provisioning immediately when resolution succeeds.
Can I use Let's Encrypt for thousands of custom domains on my SaaS?
Yes, but you need to plan around rate limits. The main constraint is 50 certificates per registered domain per week (for your own platform subdomains) and 300 new orders per 3 hours. For customer custom domains, each domain is a separate registered domain, so the per-domain limit doesn't apply the same way. You can also apply to Let's Encrypt for a rate limit increase if you can demonstrate legitimate need. ZeroSSL is a solid fallback ACME provider.
What's the difference between wildcard subdomains and custom domains?
Wildcard subdomains (*.yourapp.com) are controlled entirely by you -- one DNS record, one SSL certificate, simple routing. Custom domains (portal.customer.com) are domains controlled by your customers, requiring them to configure DNS and you to provision individual SSL certificates. Most SaaS products start with wildcard subdomains and add custom domain support later as a premium feature.
How do I handle apex (bare) custom domains?
Apex domains (customer.com without www) can't use CNAME records per the DNS spec. Your options are: have customers create A records pointing to your IP, use DNS providers that support ALIAS/ANAME records, or recommend providers with CNAME flattening like Cloudflare. Always support both apex and subdomain configurations -- customers will want both.
Should I use Caddy or nginx for automated SSL?
If you're starting fresh, use Caddy. Its on-demand TLS feature was literally built for the multi-tenant custom domain use case. Nginx is fine if you already have it in your stack, but you'll need to build more automation around certbot for certificate lifecycle management. Caddy handles issuance, renewal, storage, and OCSP stapling automatically.
How do I prevent abuse of my custom domain feature?
Three layers: First, verify domain ownership with a TXT record before accepting a custom domain. Second, implement an "ask" endpoint (like Caddy's on_demand_tls ask) that validates domains before requesting certificates. Third, rate limit domain additions per account. Without these, someone could point thousands of domains at your IP and burn through your certificate rate limits or use your infrastructure for domain fronting.
What happens if a customer removes their DNS record?
Your platform should detect this within hours through regular health checks. When DNS stops resolving to your infrastructure, mark the domain as unhealthy, notify the customer, and stop attempting certificate renewal. Don't delete the domain configuration immediately -- customers sometimes temporarily break DNS during migrations. I recommend a 30-day grace period before fully deactivating a custom domain.
Is Cloudflare for SaaS worth it compared to self-managing certificates?
For most SaaS products, yes. At $0.10/hostname/month, the cost is trivial compared to the engineering time you'd spend building and maintaining certificate automation, handling edge cases, monitoring renewals, and dealing with rate limits. You also get a global CDN and DDoS protection included. The main reason to self-manage is if you need full control over your infrastructure or have specific compliance requirements that prevent using a third-party proxy. If you want to discuss the right approach for your specific product, reach out to us -- we've implemented both patterns.
Custom domains are one of those SaaS features where the devil really is in the details. The happy path is straightforward, but production readiness means handling DNS propagation delays, certificate failures, customer misconfiguration, and monitoring at scale. Start with wildcard subdomains, add custom domain support when customers ask for it (and they will), and don't try to build everything from scratch when tools like Caddy and Cloudflare for SaaS exist. Your future on-call self will thank you.