Je klant typt app.theircustomer.com in hun browser in en wacht. Achter die drie seconden laden orchestreert je infrastructuur een DNS lookup, valideert een wildcard certificaat, routeert door je reverse proxy, en serveert hun branded dashboard — allemaal zonder dat ze je werkelijke server IP kennen. Ik heb dit custom domain pattern vier keer shipped over verschillende stacks, en elke implementatie bracht gotchas aan het licht die nooit in de product spec verschenen: Let's Encrypt rate limits die je tiende customer onboarding blokkeren, CNAME flattening behavior die breekt op specifieke DNS providers, propagation delays die support tickets doen opstapelen tijdens launch week. Je architekt dit eenmalig denkend dat het een twee-sprint project is — voordat je de edge cases ontdekt die SSL automation omzetten in een weekend van Cloudflare API debugging en certificate chain validation.

Dit artikel is de playbook die ik had willen hebben. We gaan wildcard subdomains voor multi-tenant apps behandelen, automated SSL provisioning met Let's Encrypt en alternatieven, custom vanity domains waar klanten hun eigen domein meenemen, en de operationele concerns die je om 2 uur 's nachts zullen bijten als je er niet op plant.

Inhoudsopgave

Custom Domains voor SaaS: Wildcard Subdomains & Automated SSL

Waarom Custom Domains Belangrijk zijn voor SaaS

Custom domains zijn niet alleen een vanity feature. Het zijn een trust signal. Wanneer de klant van je klant portal.acmecorp.com bezoekt in plaats van acmecorp.your-saas.io, versterkt het het merk van je klant. Het verwijdert friction. En voor veel enterprise deals is het een hard requirement — het procurement team keurt software niet goed die werknemers op een derde-party domein dwingt.

Er is ook een SEO hoek. Als je een SaaS bouwt die public-facing pages betreft (denk Shopify stores, landing page builders, knowledge bases, of client portals), hebben klanten hun eigen domeinen nodig om domain authority op te bouwen. Je kan dat niet doen op een subdomain van andermans platform. Nou, je kan, maar het is suboptimaal.

De drie meest voorkomende patterns die ik zie:

  1. Platform subdomainscustomer.your-app.com (makkelijkst)
  2. Custom subdomainsapp.customer.com (medium complexity)
  3. Apex custom domainscustomer.com (hardst, omdat CNAME records niet werken op de apex)

Meeste volwassen SaaS producten ondersteunen uiteindelijk alle drie.

Architecture Overview: Drie Benaderingen

Voordat we in implementatie details gaan, kijken we naar de drie hoofd architecturale benaderingen en wanneer elk logisch is.

Benadering Complexity Best voor SSL Method DNS Requirement
Wildcard subdomain Laag Platform-controlled subdomains Single wildcard cert Wildcard DNS A/AAAA record
Reverse proxy met SNI Medium Custom domains op matige schaal Per-domain cert via ACME Customer CNAME of A record
CDN/Edge met custom domains Laag-Medium Hoge schaal, globale distributie Managed door CDN provider Customer CNAME of A record
Dedicated load balancer per tenant Hoog Enterprise isolation requirements Per-tenant cert Customer DNS delegation

Voor meeste SaaS applicaties start je met wildcard subdomains en voeg je uiteindelijk reverse-proxy-based custom domain support toe. Laten we dieper ingaan op elk.

Wildcard Subdomains voor Multi-Tenancy

Dit is je startpunt. Elke tenant krijgt {slug}.yourapp.com. Hier is hoe je het correct opzet.

DNS Configuration

Je hebt een wildcard DNS record nodig:

*.yourapp.com.  300  IN  A      203.0.113.10
*.yourapp.com.  300  IN  AAAA   2001:db8::1

Dit betekent dat elk subdomain van yourapp.com naar je server resolveert. Je applicatie leest dan de Host header om te bepalen welke tenant te serveren.

Application-Level Routing

In een Next.js app (die we veel bouwen bij Social Animal), zou je dit in middleware afhandelen:

// 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).*)']
};

Voor Astro-based sites (nog een framework die we zwaar gebruiken), zou je dit in je server middleware of aan de edge afhandelen.

Wildcard SSL Certificate

Een enkel wildcard certificaat dekt alle subdomains af:

certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d 'yourapp.com' \
  -d '*.yourapp.com'

Opmerking: wildcard certificaten van Let's Encrypt vereisen DNS-01 challenge validatie. Je kan HTTP-01 niet voor wildcards gebruiken. Dit betekent dat je API access tot je DNS provider nodig hebt. Certbot heeft plugins voor Cloudflare, Route53, Google Cloud DNS, en meeste grote providers.

Let's Encrypt wildcard certs zijn 90 dagen geldig, dus automatiseer die renewal. Echt. Zet een monitoring alert als renewal failt — wees niet de persoon die 500 customer sites offline haalt omdat een cert verlopen is.

Custom Domains voor SaaS: Wildcard Subdomains & Automated SSL - architecture

Automated SSL met Let's Encrypt

Wanneer klanten hun eigen domeinen meenemen, heb je per-domain certificaten nodig. Dit is waar het interessant wordt.

Het ACME Protocol

Let's Encrypt gebruikt het ACME (Automatic Certificate Management Environment) protocol. De twee challenges waar je om zal geven:

  • HTTP-01: Je bewijst domein ownership door een specifiek bestand te serveren op http://yourdomain.com/.well-known/acme-challenge/{token}. Makkelijkst te automatiseren, werkt voor individuele domeinen.
  • DNS-01: Je bewijst ownership door een specifieke TXT record aan te maken. Vereist voor wildcards, moeilijker te automatiseren voor customer domeinen (je controleert hun DNS niet).

Voor custom domains gebruik je vrijwel altijd HTTP-01. De flow ziet er zo uit:

  1. Klant voegt een CNAME toe die naar je platform wijst
  2. Je systeem detecteert dat de DNS correct resolveert
  3. Je initieert een ACME certificaat request
  4. Let's Encrypt stuurt een HTTP-01 challenge
  5. Je server antwoordt met de correcte challenge token
  6. Certificaat wordt verleend en opgeslagen
  7. Je reverse proxy begint HTTPS te serveren voor dat domein

Caddy: De Lazy (Smart) Optie

Eerlijk gezegd, als je niet al committed bent aan nginx, gebruik gewoon Caddy. Het handelt automatische HTTPS out of the box af, inclusief on-demand TLS voor onbekende domeinen:

{
  on_demand_tls {
    ask http://localhost:5555/check-domain
    interval 2m
    burst 5
  }
}

https:// {
  tls {
    on_demand
  }
  reverse_proxy localhost:3000
}

De ask endpoint is kritiek — dit is waar Caddy met je applicatie controleert of een domein werkelijk een valid customer domein is voordat een certificaat wordt aangevraagd. Zonder dit, zou iedereen hun domein naar je IP kunnen wijzen en certificaat requests triggeren, potentieel je Let's Encrypt rate limits opbranden.

// /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 Benadering

Als je al nginx draait, kan je certbot gebruiken met de webroot plugin:

certbot certonly --webroot -w /var/www/certbot \
  -d customer-domain.com \
  --non-interactive --agree-tos \
  --email ssl@yourapp.com

Je moet nginx configs dynamisch updaten en herladen:

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;
    }
}

Dit werkt maar is pijnlijk om op schaal te beheren. Elk nieuw domein betekent een config genereren, een cert aanvragen, en nginx herladen. Je wilt automation rond dit bouwen, en eerlijk gezegd is Caddy's on-demand TLS gewoon makkelijker.

Let's Encrypt Rate Limits (2025)

Deze hebben eraan toe en je moet eromheen plannen:

Limit Value Notes
Certificates per Registered Domain 50/week Per je root domein
Duplicate Certificate 5/week Dezelfde set hostnames
Failed Validations 5/hour per account per hostname Kan je snel blokkeren
New Orders 300/3 hours Account-wide
Pending Authorizations 300/account Schoon oude op

Bij 50 certs per week, als je meer dan ~7 custom domeinen per dag aan het onboarden bent, moet je hierover nadenken. Opties:

  • Gebruik een ander ACME CA (ZeroSSL, BuyPass) als fallback
  • Vraag een Let's Encrypt rate limit increase aan (ze geven deze voor legitimate SaaS use cases)
  • Pre-valideer DNS voordat je certificaat issuance probeert
  • Implementeer retry logic met exponential backoff

Custom Vanity Domains: De Volledige Flow

Hier is de complete user journey die ik aanbeveel, na dit flow meerdere keren te hebben gebouwd.

Stap 1: Klant Voert Domein in Je Dashboard In

Gevens duidelijke instructies. Toon hen exact welke DNS record aan te maken:

Type: CNAME
Host: portal (of wat subdomain ze willen)
Value: custom.yourapp.com
TTL: 300

Voor apex domeinen (bare customer.com), hebben ze een A record nodig naar je IP, of ze hebben een DNS provider nodig die CNAME flattening ondersteunt (Cloudflare, Route53 ALIAS records, enz).

Stap 2: DNS Verification

Controleert niet eenmalig. DNS propagatie kan minuten tot uren duren. Implementeer een 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;
}

Stap 3: Certificate Provisioning

Zodra DNS verified is, vraag het certificaat aan. Met Caddy gebeurt dit automatisch op eerste request. Met nginx/certbot, trigger het programmatisch.

Stap 4: Ongoing Monitoring

Domeinen kunnen breken. Klanten veranderen DNS records per ongeluk. Certificaten verlopen als renewal failt. Je moet monitoren:

  • DNS resolution wijst nog steeds naar jou (controleer dagelijks)
  • Certificate expiry dates (alert op 14 dagen, kritiek op 7)
  • SSL handshake success (synthetic monitoring)
  • HTTP response codes op customer domeinen

Infrastructure Patterns per Platform

De benadering varieert aanzienlijk afhankelijk van waar je hosted.

Vercel

Vercel heeft ingebouwde custom domain support. Voor een multi-tenant Next.js app, zou je hun Domains API gebruiken:

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 handelt SSL automatisch af. De hoofd limitatie: je bent onderhevig aan hun pricing. Op het Pro plan ($20/team member/month), krijg je 50 domeinen per project. Enterprise geeft je meer. Als je duizenden klanten met custom domeinen hebt, stapelen de kosten op.

Cloudflare for SaaS (SSL for SaaS)

Dit is waarschijnlijk de beste optie voor schaal in 2025. Cloudflare's SSL for SaaS product (eerder Custom Hostnames genoemd) handelt alles af:

  • Automatische certificaat issuance en renewal
  • Globaal CDN en DDoS protection
  • Ingebouwde domein verification
  • API voor programmatische 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 na de eerste 100 (die gratis zijn op het Enterprise plan). Voor een SaaS met 1,000 custom domeinen, dat's ongeveer $90/month. Erg redelijk.

AWS (ALB + ACM + Route53)

Als je op AWS bent, kan je gebruiken:

  • ALB voor routing met SNI-based certificaat selectie
  • ACM (AWS Certificate Manager) voor gratis certificaten
  • Route53 voor DNS verificatie

De haken: ACM certificaten zijn alleen bruikbaar met AWS services (ALB, CloudFront, API Gateway). En ALB heeft een limit van 25 certificaten per load balancer standaard (kan verhoogd worden naar 100). Voor serieuze schaal zou je CloudFront ervoor zetten met zijn limit van 100 certificaten per distribution.

Dit wordt duur en complex. Ik zou eerlijk CloudFlare for SaaS over deze benadering aanbevelen tenzij je specifieke AWS requirements hebt.

Fly.io

Fly.io heeft een fijne fly certs add commando en API voor custom domeinen toe te voegen:

fly certs add portal.customer.com

Het handelt Let's Encrypt automatisch af. Werkt goed voor kleine tot middelmatige schaal.

DNS Configuration en Verification

De DNS piece struikelt meer teams dan enig ander deel van deze feature. Hier is wat je moet weten.

Het CNAME-at-Apex Probleem

Klanten willen onvermijdelijk hun bare domein gebruiken (customer.com, geen subdomain). De DNS spec staat geen CNAME records op de apex van een zone toe omdat CNAME records exclusief moeten zijn — ze kunnen niet naast andere record types bestaan, en de apex heeft altijd SOA en NS records.

Oplossingen:

  1. CNAME flattening (Cloudflare) — resolveert de CNAME op DNS niveau, retourneert een A record
  2. ALIAS records (Route53, DNSimple) — proprietary record type dat werkt als een CNAME op de apex
  3. ANAME records (sommige providers) — vergelijkbaar met ALIAS
  4. A record — geef klanten gewoon je IP adres

Optie 4 (A records) is het meest universeel maar het minst flexibel. Als je server IP ooit verandert, moet elke klant met een A record hun DNS updaten. Met een CNAME update je gewoon waar je CNAME target naar resolveert.

Mijn aanbeveling: ondersteun beide. Zeg klanten een CNAME voor subdomains en een A record (of ALIAS/ANAME als hun provider het ondersteunt) voor apex domeinen te gebruiken.

Ownership Verification

Buiten bevestiging dat het domein naar je infrastructuur resolveert, wil je misschien verifiëren dat de persoon die het domein in je SaaS configureert het werkelijk controleert. Een gebruikelijke benadering is een TXT record vereisen:

_verification.customer.com  TXT  "yourapp-verify=abc123unique"

Dit voorkomt dat iemand een domein die ze niet bezitten naar je platform wijst en beweert dat het tot hun account behoort.

Production Hardening en Monitoring

Deze sectie is het verschil tussen een demo en een production system.

Certificate Storage

Sla certificaten niet op disk op als je meerdere instances draait. Gebruik een distributed store:

  • Caddy: ondersteunt storage backends voor Redis, Consul, S3, PostgreSQL
  • Custom: sla certs op in een versleuterde database of secrets manager (AWS Secrets Manager, HashiCorp Vault)

Graceful Fallback

Wat gebeurt er wanneer het domein van een klant is misconfigureerd? Toon geen SSL error. In plaats daarvan:

  1. Detecteer de SSL handshake failure
  2. Redirect naar een status page die het probleem uitlegt
  3. Stuur een notificatie naar de klant
  4. Probeer automatisch certificaat provisioning opnieuw

Health Checks

Bouw een background job die regelmatig elk custom domein controleert:

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 points no longer 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(),
    });
  }
}

Draai dit elk uur. Bij 10,000 domeinen compleet de checks in enkele minuten als je goed paralleliseert.

Cost Analysis op Schaal

Laten we werkelijke 2025 prijzen bespreken.

Oplossing 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 Je ops burden
Cloudflare for SaaS Gratis (Enterprise) ~$90/mo ~$990/mo Beste value op schaal
Vercel (Pro) Inbegrepen $0 extra Need Enterprise Limited to 50/project op Pro
AWS CloudFront + ACM ~$100/mo ~$300/mo ~$2,000/mo Bevat CDN transfer kosten
Fastly ~$150/mo ~$500/mo Custom pricing Goed als je al Fastly gebruikt

Voor meeste SaaS producten raakt Cloudflare for SaaS het sweet spot. Je krijgt globaal CDN, DDoS protection, en automatische certificaten voor ongeveer een dubbeltje per domein per maand. Lastig om beter te doen dan dat.

Als je op een headless CMS architectuur bouwt — iets wat we veel doen — past Cloudflare for SaaS bijzonder goed aan omdat je al dealt met decoupled frontends die baat hebben van edge caching.

FAQ

Hoe lang duurt het voordat een custom domein actief is na DNS configuratie?

DNS propagatie duurt typisch 5-30 minuten, alhoewel het in zeldzame gevallen tot 48 uur kan duren. Certificate provisioning met Let's Encrypt voegt nog eens 30-60 seconden toe zodra DNS correct resolveert. In praktijk zijn meeste custom domeinen volledig actief binnen 15 minuten. Ik raad aan DNS elk 30 seconden te pollen en certificaat provisioning onmiddellijk te triggeren wanneer resolution succesvol is.

Kan ik Let's Encrypt gebruiken voor duizenden custom domeinen op mijn SaaS?

Ja, maar je moet rate limits inplannen. De hoofd constraint is 50 certificaten per geregistreerd domein per week (voor je eigen platform subdomains) en 300 nieuwe orders per 3 uren. Voor customer custom domeinen is elk domein een apart geregistreerd domein, dus de per-domain limit is niet op dezelfde manier van toepassing. Je kan ook Let's Encrypt benaderen voor een rate limit increase als je legitimate need kan aantonen. ZeroSSL is een solid fallback ACME provider.

Wat is het verschil tussen wildcard subdomains en custom domeinen?

Wildcard subdomains (*.yourapp.com) zijn volledig door jou gecontroleerd — één DNS record, één SSL certificaat, simple routing. Custom domeinen (portal.customer.com) zijn domeinen gecontroleerd door je klanten, vereisend dat zij DNS configureren en jij per-domain SSL certificaten provisioned. Meeste SaaS producten starten met wildcard subdomains en voegen later custom domain support toe als premium feature.

Hoe handel ik apex (bare) custom domeinen af?

Apex domeinen (customer.com zonder www) kunnen CNAME records niet gebruiken per de DNS spec. Je opties zijn: hebben klanten A records aanmaken naar je IP, DNS providers gebruiken die ALIAS/ANAME records ondersteunen, of providers aanbevelen met CNAME flattening zoals Cloudflare. Ondersteun altijd zowel apex als subdomain configuraties — klanten willen beide.

Moet ik Caddy of nginx gebruiken voor automated SSL?

Als je van scratch start, gebruik Caddy. Zijn on-demand TLS feature was letterlijk gebouwd voor het multi-tenant custom domain use case. Nginx is fijn als je het al in je stack hebt, maar je zult meer automation rond certbot moeten bouwen voor certificate lifecycle management. Caddy handelt issuance, renewal, storage, en OCSP stapling automatisch af.

Hoe voorkom ik misbruik van mijn custom domain feature?

Drie lagen: Ten eerste, verifieer domein ownership met een TXT record voordat je een custom domein accepteert. Ten tweede, implementeer een "ask" endpoint (zoals Caddy's on_demand_tls ask) die domeinen valideert voordat certificaten worden aangevraagd. Ten derde, rate limit domein toevoegingen per account. Zonder dit, zou iemand duizenden domeinen naar je IP kunnen wijzen en certificaat requests branden of je infrastructuur gebruiken voor domain fronting.

Wat gebeurt er als een klant zijn DNS record verwijdert?

Je platform moet dit binnen uren detecteren via regular health checks. Wanneer DNS stopt met resolving naar je infrastructuur, markeer het domein als unhealthy, notificeer de klant, en stop met certificaat renewal pogingen. Verwijder de domein configuratie niet onmiddellijk — klanten breken soms tijdelijk DNS tijdens migraties. Ik aanbeveel een 30-daagse grace period voordat je een custom domein volledig deactiveert.

Is Cloudflare for SaaS het waard vergeleken met self-managing certificaten?

Voor meeste SaaS producten, ja. Bij $0.10/hostname/month zijn de kosten triviaal vergeleken met de engineering tijd die je zou besteden aan het bouwen en onderhouden van certificate automation, edge cases afhandelen, renewal monitoring, en rate limit dealing. Je krijgt ook een globaal CDN en DDoS protection inbegrepen. De hoofd reden om self-managing is als je volledige controle over je infrastructuur nodig hebt of specifieke compliance requirements hebt die het gebruiken van een derde-party proxy voorkomen. Als je de juiste benadering voor je specifieke product wilt bespreken, neem contact op — we hebben beide patterns geïmplementeerd.

Custom domeinen zijn één van die SaaS features waar de duivel echt in de details zit. Het happy path is straightforward, maar production readiness betekent DNS propagatie delays, certificaat failures, customer misconfiguratie, en monitoring op schaal afhandelen. Start met wildcard subdomains, voeg custom domain support toe wanneer klanten erom vragen (en ze zullen), en probeer niet alles van scratch te bouwen wanneer tools zoals Caddy en Cloudflare for SaaS bestaan. Je toekomstige on-call zelf zal je bedanken.