Custom Domain Setup

9 min readUpdated 29 Apr 2026

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.

StepActorTime
Enter domain in Admin → Settings → Custom DomainYouinstant
VeloCMS creates Custom Hostname via CF APIVeloCMS< 5 seconds
You add CNAME record in your DNS providerYou< 5 minutes
Cloudflare verifies DNS propagationCloudflare1–30 minutes
Cloudflare issues DV certificateCloudflare5–30 minutes
Domain goes liveCloudflareautomatic

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.

DNS record to add at your registrar
# 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.
bash

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.

StatusColorMeaningWhat to do
liveGreenCNAME verified + SSL active, serving trafficNothing — you're done
pending_dnsYellowCNAME not yet seen by CloudflareAdd or verify the CNAME record, then click Retry
pending_sslYellowCNAME verified, DV certificate being issuedWait 5–30 minutes, click Retry
deactivatedRedDomain blocked or SSL timed outSee troubleshooting section below
unknownGrayTransient or unrecognized CF stateWait 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.

src/lib/cloudflare/domain-status.ts (simplified)
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
}
typescript

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 / ResourcePurpose
src/lib/cloudflare/domain-status.tsinterpretDomainStatus() — pure state interpreter
src/lib/cloudflare/custom-hostnames.tsCF API helpers: create, get, find-by-name, delete
src/lib/cloudflare/client.tscloudflareRequest() fetch wrapper with auth + error normalization
src/lib/cloudflare/types.tsCloudflareCustomHostname TypeScript types
Cloudflare for SaaS docshttps://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/