Custom Domain Setup
Every VeloCMS blog ships with a free subdomain on velocms.org (yoursite.velocms.org). Pro, Business, and Agency plans let you replace that with any domain you own — myblog.com, blog.mycompany.com, or any other hostname. The connection is powered by Cloudflare for SaaS, which provisions a DV certificate for your domain and routes traffic to VeloCMS without requiring you to transfer your DNS to Cloudflare.
How it works
When you add a custom domain in Admin → Settings → Custom Domain, VeloCMS calls the Cloudflare for SaaS API to create a Custom Hostname entry. Cloudflare then waits for your CNAME record to propagate, verifies DNS ownership, and issues a DV certificate. Once both checks pass, your domain serves your VeloCMS blog over HTTPS. Your DNS can stay with any registrar — only one CNAME record needs to point at VeloCMS.
| Step | Actor | Time |
|---|---|---|
| Enter domain in Admin → Settings → Custom Domain | You | instant |
| VeloCMS creates Custom Hostname via CF API | VeloCMS | < 5 seconds |
| You add CNAME record in your DNS provider | You | < 5 minutes |
| Cloudflare verifies DNS propagation | Cloudflare | 1–30 minutes |
| Cloudflare issues DV certificate | Cloudflare | 5–30 minutes |
| Domain goes live | Cloudflare | automatic |
DNS configuration
Add a single CNAME record in your DNS provider. The target is proxy.velocms.org for all custom domains. Do not point directly at the Railway IP address — the Cloudflare proxy must be in the chain for SSL termination to work.
# For blog.yourdomain.com (subdomain)
Type: CNAME
Name: blog
Value: proxy.velocms.org
TTL: Auto (or 300 seconds)
# For yourdomain.com (apex / root domain)
# Use CNAME-flattening / ALIAS if your DNS provider supports it.
# Cloudflare DNS: add a CNAME on @ and enable Proxy (orange cloud).
# Route 53: use ALIAS record targeting proxy.velocms.org.
# Namecheap, Porkbun: use ALIAS or ANAME record type.Domain status states
The Admin → Settings panel polls the domain status every 30 seconds while provisioning is in progress. The status indicator maps Cloudflare's internal state machine — which has roughly 20 values — into four collapsed states that drive the UI color and the action message shown to the tenant.
| Status | Color | Meaning | What to do |
|---|---|---|---|
| live | Green | CNAME verified + SSL active, serving traffic | Nothing — you're done |
| pending_dns | Yellow | CNAME not yet seen by Cloudflare | Add or verify the CNAME record, then click Retry |
| pending_ssl | Yellow | CNAME verified, DV certificate being issued | Wait 5–30 minutes, click Retry |
| deactivated | Red | Domain blocked or SSL timed out | See troubleshooting section below |
| unknown | Gray | Transient or unrecognized CF state | Wait 5 minutes, click Retry |
How the status interpreter works
The mapping from CF raw state to the five UI states is handled by interpretDomainStatus() in src/lib/cloudflare/domain-status.ts. It receives the CloudflareCustomHostname object already fetched from the API and returns a DomainStatusInterpretation with a state, a human-readable action string, and a deep-link to the relevant help article. No additional CF API calls are made inside the interpreter — it is a pure function, which makes it fully unit-testable without mocks.
export function interpretDomainStatus(
cf: CloudflareCustomHostname,
fetchedAt: number = Date.now()
): DomainStatusInterpretation {
const hStatus = cf.status;
const sStatus = cf.ssl?.status ?? "initializing";
if (hStatus === "active" && sStatus === "active") {
return { state: "live", action: "your domain is live and serving traffic over HTTPS", ... };
}
if (hStatus === "pending" || hStatus === "test_failed") {
return { state: "pending_dns", action: "add a CNAME record pointing to proxy.velocms.org", ... };
}
if (isSslPending(sStatus)) {
// If pending > 24 hours, surface a stronger message
const ageMs = fetchedAt - Date.parse(cf.created_at ?? "0");
if (ageMs > 24 * 60 * 60 * 1000) {
return { state: "pending_ssl", action: "SSL pending over 24h — remove and re-add domain", ... };
}
return { state: "pending_ssl", action: "SSL certificate being issued (usually under 30 minutes)", ... };
}
// ... other branches
}Troubleshooting
Status stuck on pending_dns after 30 minutes
- Check that the CNAME record is published: dig CNAME blog.yourdomain.com — the answer should show proxy.velocms.org
- DNS TTL may be high. If there was a previous A record on this name with TTL 86400, it can take up to 24 hours to expire globally. Lower the TTL to 300 before making the change next time.
- If you used an IP address instead of a CNAME, delete that A record and add the CNAME instead.
- Click Retry in the Admin panel to force an immediate CF verification check.
Status stuck on pending_ssl after 2 hours
- Check for CAA records on your domain: dig CAA yourdomain.com. If a CAA record restricts issuance to a specific CA (e.g., letsencrypt.org only), Cloudflare's DigiCert-issued certificate will be blocked. Add 0 issue digicert.com or remove the CAA restriction.
- Apex domain: verify CNAME-flattening is enabled (Cloudflare Proxy or Route 53 ALIAS). Plain CNAME on apex won't propagate.
- If still stuck after 24 hours: delete the domain in Admin settings and re-add it. This resets the CF provisioning workflow.
Status shows deactivated (error code D-001 or D-002)
- D-001: Cloudflare blocked the hostname at the registrar level (rare — happens if the domain is on Cloudflare's abuse blocklist). Contact [email protected] with the domain name.
- D-002: SSL issuance timed out and entered a terminal state. Delete the domain in Admin settings and re-add it to start a fresh provisioning cycle. If it times out again, check your CAA records first.
Removing a custom domain
Go to Admin → Settings → Custom Domain and click Remove. VeloCMS calls the CF API to delete the Custom Hostname entry and updates the tenant record. Your blog reverts to yoursite.velocms.org immediately. You can safely delete the CNAME record from your DNS after removal. Removing and re-adding a domain restarts the full provisioning flow and is the recommended fix for stuck SSL states.
www vs apex
The most reliable configuration is to point the www subdomain at VeloCMS and redirect the apex to www at your DNS provider or registrar. This avoids the CNAME-at-apex limitation entirely. If you want the apex to be the primary URL, use a DNS provider with CNAME-flattening and test with dig before submitting the domain in Admin settings.
Reference
| File / Resource | Purpose |
|---|---|
| src/lib/cloudflare/domain-status.ts | interpretDomainStatus() — pure state interpreter |
| src/lib/cloudflare/custom-hostnames.ts | CF API helpers: create, get, find-by-name, delete |
| src/lib/cloudflare/client.ts | cloudflareRequest() fetch wrapper with auth + error normalization |
| src/lib/cloudflare/types.ts | CloudflareCustomHostname TypeScript types |
| Cloudflare for SaaS docs | https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/ |