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. 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).
| Path | Where your DNS is | Main address | Traffic record | Sibling 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 it | Proxied 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 provider | A provider that supports ALIAS or ANAME on the root | Root domain (checkbox ticked) | ALIAS @ to the target | The 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 registrar | A registrar with no root flattening or ALIAS | www (checkbox unticked) | CNAME www to the target | The 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
| Step | Actor | Time |
|---|---|---|
| Enter the domain in the Custom domain section of Admin → Settings → Branding and click Add Domain | You | instant |
| VeloCMS registers the domain and shows the traffic record and the ownership TXT | VeloCMS | < 5 seconds |
| You add the ownership TXT at your DNS provider | You | < 5 minutes |
| The platform verifies ownership and starts the SSL certificate | Hosting platform | 1–30 minutes |
| You switch the traffic record, then add the sibling redirect | You | < 5 minutes |
| Domain goes live | Hosting platform | automatic |
DNS records per path
# 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 onType: 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)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)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.
- 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.
- Add the domain in Admin and copy the target and the ownership TXT from the panel. Nothing public changes.
- Add the ownership TXT only. Leave the traffic records pointing at the old site.
- 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.
- 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.
- Once the main address loads, add the sibling redirect and test it with a deep page and a query string.
- 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.
EDGE=$(dig +short <target-from-your-panel> | head -1)
curl -sI --resolve example.com:443:$EDGE https://example.com/ | head -8A 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.
| Status | Color | Meaning | What to do |
|---|---|---|---|
| live / Active | Green | DNS verified + SSL issued, serving traffic | Nothing — you're done |
| pending_dns / Pending DNS | Yellow | Ownership TXT or traffic record not yet seen | Add or fix the records, then click Verify Now |
| pending_ssl / Verifying | Yellow | DNS verified, SSL certificate being issued | Wait a few minutes, click Verify Now again |
| deactivated / Failed | Red | Certificate issuance failed | Disconnect and re-add the domain, or see troubleshooting below |
| unknown | Gray | Transient or unclear state | Wait 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.
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
}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 / Resource | Purpose |
|---|---|
| src/lib/railway/domains.ts | Platform API helpers: createRailwayDomain, getRailwayDomain, deleteRailwayDomain, certificate issuance |
| src/lib/railway/domain-status.ts | interpretRailwayStatus() — pure state interpreter |
| src/lib/railway/client.ts | railwayGraphQL() fetch wrapper with auth + error normalization |
| src/lib/cloudflare/dns-instructions.ts | buildDnsInstructions() — per-path record instructions (subdomain, Cloudflare root, ALIAS root, standard registrar) |
| src/lib/actions/custom-domain.ts | addCustomDomain (apexMode www or apex), refreshDomainStatus, retryDomainVerification, checkDomainDns, removeCustomDomain |
| docs/specs/2026-07-15-custom-domain-railway-routing.md | Architecture decision spec for the current routing model |