Custom Domain Setup

11 min readUpdated 29 Oct 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. When you connect a domain, VeloCMS registers it with our hosting platform, which assigns the domain its own CNAME target plus an ownership TXT record and issues a free SSL certificate once your DNS is in place. Your DNS stays with your current provider; you only add the records the panel shows you.

Pick your path

A subdomain (blog.example.com) needs one CNAME record plus the ownership TXT record on every provider. A root domain (example.com) cannot be pointed at another host by a plain registrar, so the path depends on where your DNS is hosted. The Custom domain panel in Admin → Settings → Branding (also at /admin/setup/custom-domain) shows a checkbox, "Use the root domain as the main address", as soon as you type a root domain. It decides which host VeloCMS registers: the root domain itself (Paths 1 and 2) or www.example.com (Path 3).

PathWhere your DNS isMain addressTraffic recordSibling redirect
1. Cloudflare (recommended for a root-domain main address)Cloudflare (free plan is enough)Root domain (checkbox ticked)CNAME @ to the target, DNS only (grey cloud); Cloudflare flattens itProxied A record www to 192.0.2.1 (placeholder, never contacted) plus one Single Redirect rule from www to the root, 301, preserve query string
2. ALIAS / ANAME providerA provider that supports ALIAS or ANAME on the rootRoot domain (checkbox ticked)ALIAS @ to the targetThe provider's URL redirect from www to the root, only if it serves HTTPS for www and keeps path and query; otherwise use Path 1
3. Standard registrarA registrar with no root flattening or ALIASwww (checkbox unticked)CNAME www to the targetThe registrar's forwarding from the root to https://www.example.com; run the HTTPS check below, and move DNS to Cloudflare if it fails

Every path also needs the ownership TXT record. Its name looks like _railway-verify.<label> and the panel shows the exact name and value; it is required, and the domain will not verify with only the traffic record.

How it works

StepActorTime
Enter the domain in the Custom domain section of Admin → Settings → Branding and click Add DomainYouinstant
VeloCMS registers the domain and shows the traffic record and the ownership TXTVeloCMS< 5 seconds
You add the ownership TXT at your DNS providerYou< 5 minutes
The platform verifies ownership and starts the SSL certificateHosting platform1–30 minutes
You switch the traffic record, then add the sibling redirectYou< 5 minutes
Domain goes liveHosting platformautomatic

DNS records per path

Path 1: Cloudflare, root domain as the main address
# Ownership (add this first)
Type:  TXT
Name:  _railway-verify.<label>      # exact name shown in your panel
Value: <the token shown in your panel>

# Traffic
Type:  CNAME
Name:  @
Value: <the per-domain target shown in your panel>
Proxy: DNS only (grey cloud)

# Sibling host
Type:  A
Name:  www
Value: 192.0.2.1                    # placeholder, never contacted
Proxy: Proxied (orange cloud)

# Rules → Redirect Rules → Single Redirect
Match:    https://www.example.com/*
Redirect: https://example.com/${1}
Status:   301, Preserve query string on
bash
Path 2: ALIAS / ANAME, root domain as the main address
Type:  TXT
Name:  _railway-verify.<label>      # exact name shown in your panel
Value: <the token shown in your panel>

Type:  ALIAS (or ANAME)
Name:  @
Value: <the per-domain target shown in your panel>

# Provider's URL redirect (not a DNS record)
www.example.com  ->  https://example.com   (301, keep path and query)
bash
Path 3: standard registrar, www as the main address
Type:  TXT
Name:  _railway-verify.<label>      # exact name shown in your panel
Value: <the token shown in your panel>

Type:  CNAME
Name:  www
Value: <the per-domain target shown in your panel>

# Registrar forwarding (not a DNS record)
example.com  ->  https://www.example.com   (301, keep path and query)
bash

The target is unique to your domain and looks like a VeloCMS hostname built from your domain, for example www-example-com.domains.velocms.org. Always copy it from the panel; never point at an IP address.

The safe order for a site that is already live

Replacing a live site goes in this order. Every step ends in a check and every step can be undone.

  1. Save a copy of every DNS record the domain has today (zone export, or a screenshot of every page of the table, including email records). A day or more ahead, lower the TTL of the records you will change to 300 seconds.
  2. Add the domain in Admin and copy the target and the ownership TXT from the panel. Nothing public changes.
  3. Add the ownership TXT only. Leave the traffic records pointing at the old site.
  4. Click Verify Now and use Check DNS records to compare what is live with what the panel expects. Repeat until the domain is verified and the certificate is issued or being issued. Verify Now also asks the platform to issue the certificate when it looks stalled. The panel may keep a pending state until the traffic record points at us.
  5. Switch the traffic record in a quiet hour. Edit the existing record in place (for example the root A record into a CNAME) instead of deleting and re-adding it, so the name is never empty.
  6. Once the main address loads, add the sibling redirect and test it with a deep page and a query string.
  7. Check: the panel shows Active, both addresses open with path and query intact, and the browser shows a valid padlock.

An optional technical check before step 5: talk to the hosting edge directly, bypassing DNS. Resolve the target from your panel to an address, then request your domain from that address.

Optional pre-switch check
EDGE=$(dig +short <target-from-your-panel> | head -1)
curl -sI --resolve example.com:443:$EDGE https://example.com/ | head -8
bash

A healthy answer is 200, or a redirect on the same host, with no x-railway-fallback header and a certificate that is valid for example.com. A generic *.up.railway.app certificate or an x-railway-fallback header means the domain is not routed yet: repeat Verify Now and do not switch.

How to roll back

Until you retire the old site, undoing the switch is a DNS edit. Restore the saved records one at a time, in the reverse order you changed them: the sibling redirect first (disable the rule, put www back), then the main traffic record (edit it back in place to its old type, value, and proxy setting). With a 300-second TTL the old site is back within about five minutes. The ownership TXT is harmless and can stay. Do not click Disconnect while a traffic record still points at the target: restore DNS first, then disconnect if you want the domain gone. Leaving the domain added keeps a retry fast.

Keep the old site live until a real order has gone through

Do not cancel the old shop or host, remove its domain connection, or announce that it is gone until a real customer order has gone through the new site end to end: the customer paid, the payment shows in your payment provider's dashboard, the order appears in your admin, and the confirmation email reached the customer's inbox rather than spam. A test purchase of your own proves the pipeline but not that a stranger can find you and buy through the new domain. Export what only the old platform holds (orders, customers, gift-card balances, discount codes) before you cancel. A sensible minimum is two weeks; if no real order arrives, keep the old site and check weekly.

Domain status states

The Custom domain panel checks the domain status when you click Verify Now and refreshes it in the background while a domain is pending or verifying. The hosting platform reports two signals, the state of your DNS records and the state of your SSL certificate, which VeloCMS maps into the status badge and the action message.

StatusColorMeaningWhat to do
live / ActiveGreenDNS verified + SSL issued, serving trafficNothing — you're done
pending_dns / Pending DNSYellowOwnership TXT or traffic record not yet seenAdd or fix the records, then click Verify Now
pending_ssl / VerifyingYellowDNS verified, SSL certificate being issuedWait a few minutes, click Verify Now again
deactivated / FailedRedCertificate issuance failedDisconnect and re-add the domain, or see troubleshooting below
unknownGrayTransient or unclear stateWait 5 minutes, click Verify Now

How the status interpreter works

The mapping from the platform's raw DNS + certificate states to the UI states is handled by interpretRailwayStatus() in src/lib/railway/domain-status.ts. It receives the RailwayDomainResult already fetched from the platform API (the per-domain CNAME target, the ownership TXT, and the DNS + certificate status strings) and returns a DomainStatusInterpretation with a state, a human-readable action string, and a deep-link to the relevant help article. No additional API calls are made inside the interpreter; it is a pure function, which makes it fully unit-testable without mocks.

src/lib/railway/domain-status.ts (simplified)
export function interpretRailwayStatus(
  result: RailwayDomainResult,
  kind: CustomDomainKind = "subdomain"
): DomainStatusInterpretation {
  const dns = (result.dnsStatus || "").toUpperCase();
  const cert = (result.certStatus || "").toUpperCase();

  if (certIssued && !dnsNeedsAction) {
    return { state: "live", action: "your domain is live and serving traffic over HTTPS", ... };
  }

  if (cert.includes("CAA")) {
    return { state: "caa_blocked", action: "a CAA record is blocking SSL issuance — allow Let's Encrypt", ... };
  }

  if (certErrored || dnsErrored) {
    return { state: "deactivated", action: "certificate issuance failed — remove and re-add the domain", ... };
  }

  if (dnsNeedsAction) {
    return { state: "pending_dns", action: "add the CNAME + ownership TXT records shown below, then click Verify", ... };
  }

  // Certificate still validating with DNS observed → pending_ssl
  // ... other branches
}
typescript

Troubleshooting

Status stuck on Pending DNS after 30 minutes

  • Check the ownership TXT first: dig TXT with the exact name from your panel must return the token shown there. The domain will not verify with only the traffic record.
  • Check the traffic record: dig CNAME for the name you created (for a root-domain ALIAS, query the root). The answer should show the exact per-domain target from your panel.
  • DNS TTL may be high. A previous A record with TTL 86400 can take up to 24 hours to expire globally. Lower the TTL to 300 before the next change.
  • If you used an IP address instead of the target, delete that A record and add the CNAME or ALIAS instead.
  • If your DNS is on Cloudflare, the traffic record must be DNS only (grey cloud), not proxied; a proxied record hides the real target.
  • Click Verify Now to force an immediate re-check.

Status stuck on Verifying after 2 hours

  • Check for CAA records on your domain: dig CAA example.com. A CAA record that does not allow Let's Encrypt (letsencrypt.org) blocks the certificate. Add letsencrypt.org or remove the restrictive record.
  • Root domain: confirm CNAME flattening (Cloudflare) or the ALIAS/ANAME record is actually in place. A plain CNAME on a root domain at a standard registrar will not work.
  • Certificates usually complete within an hour of the records going live. If still stuck after 24 hours, click Disconnect and add the domain again to start a fresh attempt.

Status shows Failed (error code D-002)

  • D-002: SSL issuance entered a terminal failure state. Disconnect the domain and add it again. If it fails again, check your CAA records first, then contact [email protected] with the domain name and code D-002.

The sibling redirect fails over HTTPS or drops the path

  • That is a limit of the registrar's or provider's forwarding service, not of your records. Redirect with a Cloudflare Single Redirect rule instead (Path 1) and repeat the HTTPS check.

Disconnecting a custom domain

Go to Admin → Settings → Branding, find the Custom domain section, and click Disconnect. VeloCMS de-registers the domain from our hosting platform and clears it from your site. Your blog reverts to yoursite.velocms.org immediately. If the domain still serves a live site, restore your old DNS records before disconnecting. Afterwards you can delete the traffic and TXT records. Disconnecting and re-adding restarts the full provisioning flow and is the recommended fix for stuck SSL states, and the way to change between a root-domain and a www main address.

Reference

File / ResourcePurpose
src/lib/railway/domains.tsPlatform API helpers: createRailwayDomain, getRailwayDomain, deleteRailwayDomain, certificate issuance
src/lib/railway/domain-status.tsinterpretRailwayStatus() — pure state interpreter
src/lib/railway/client.tsrailwayGraphQL() fetch wrapper with auth + error normalization
src/lib/cloudflare/dns-instructions.tsbuildDnsInstructions() — per-path record instructions (subdomain, Cloudflare root, ALIAS root, standard registrar)
src/lib/actions/custom-domain.tsaddCustomDomain (apexMode www or apex), refreshDomainStatus, retryDomainVerification, checkDomainDns, removeCustomDomain
docs/specs/2026-07-15-custom-domain-railway-routing.mdArchitecture decision spec for the current routing model