Skip to content

Custom Domains

Every frontend deployment gets a *.pages.dev URL for free. A custom domain puts that same deployment on a hostname you own — app.example.com, or the apex example.com — with an SSL certificate Butterbase issues and renews for you.

Custom domains are a Launch plan or above feature.

Butterbase uses Cloudflare for SaaS. When you add a hostname:

  1. Butterbase registers it as a custom hostname with Cloudflare and stores it against your app.
  2. A KV mapping is written so the edge dispatcher knows which app that hostname belongs to.
  3. You point DNS at us and prove you own the name.
  4. Cloudflare issues a certificate. Renewal is automatic and you never touch it.

You keep control of your DNS the whole way through — nothing about this requires moving your domain to Cloudflare.

RequirementWhy
Launch plan or aboveThe custom_domain plan feature gates the endpoint. Playground apps get a 403.
The app owns the domainA hostname is globally unique across Butterbase — a second app adding the same name gets a 409.
A live frontend deploymentThe domain routes to whatever the app currently serves. Add the domain first if you like; it just won’t have anything to serve yet.
WfP deployment backendApps on the legacy backend return 400. New apps are already on WfP.
Access to your DNS providerYou must be able to create CNAME and TXT records.

validation_method decides how Cloudflare proves you own the hostname before it issues a certificate. Choosing wrong here is the single most common reason a domain sits at “pending” forever.

MethodHow it worksUse it whenDo not use it when
http (default)Cloudflare serves a challenge file from our zone; you only add the routing CNAMESubdomains, and any DNS provider that isn’t CloudflareYour DNS is a Cloudflare-proxied zone, or the hostname is an apex on Cloudflare — the orange-cloud intercept swallows the challenge, and CNAME flattening hides the record entirely
txtCloudflare gives you a TXT record to add in DNSApex domains on Cloudflare, proxied zones, or any time you want the method that always works

Rule of thumb: subdomain on a normal DNS provider → http. Anything on Cloudflare, or any apex → txt.

Dashboard: App → Settings → Custom domains → Add domain.

CLI:

Terminal window
butterbase domains add app.example.com
butterbase domains add example.com --validation-method txt

REST API:

POST /v1/{app_id}/custom-domains
Authorization: Bearer {token}
{ "hostname": "app.example.com", "validation_method": "http" }

MCP:

manage_frontend({
app_id: "app_abc123",
action: "configure_custom_domain",
domain_action: "add",
hostname: "app.example.com"
})

The 201 response carries everything you need for the next step:

{
"domain": { "id": "", "hostname": "app.example.com", "status": "pending", "ssl_status": "pending" },
"cname_target": "butterbase.dev",
"validation_method": "http",
"verification_records": [{ "txt_name": "_acme-challenge.app.example.com", "txt_value": "" }],
"ownership_verification": null,
"instructions": "Add a CNAME record at your DNS provider: …"
}

Keep domain.id — it’s what the status, verify, and delete calls take.

One record:

TypeNameValue
CNAMEapp.example.combutterbase.dev

Two records, sometimes three:

#TypeNameValue
1CNAMEapp.example.combutterbase.dev — for an apex, use whatever your provider offers at the root (Cloudflare’s flattened CNAME is fine)
2TXTverification_records[].txt_nameverification_records[].txt_value — authorizes the certificate
3TXTownership_verification.nameownership_verification.valueonly returned for Cloudflare-proxied zones; add it if present

If verification_records came back empty, Cloudflare hasn’t minted the record yet. Call the status endpoint in a minute and it will be there.

Terminal window
butterbase domains status <domain-id>
GET /v1/{app_id}/custom-domains/{domain_id}/status

The endpoint re-reads live state from Cloudflare on every call and writes it back, so it’s always current.

{
"domain": {
"hostname": "app.example.com",
"status": "active",
"ssl_status": "active",
"verification_errors": null
},
"cname_target": "butterbase.dev",
"verification_records": [],
"ownership_verification": null
}
FieldMeaning
statusCloudflare’s hostname state. pending → waiting on DNS/validation. active → routing works.
ssl_statusCertificate state. pending/initializing/pending_validation → still issuing. active → HTTPS works.
verification_errorsPopulated when Cloudflare rejected validation — read this first when something is stuck.

You’re live when status and ssl_status are both active. Typical time is 5–15 minutes; DNS propagation is usually the long pole.

If it’s stuck, re-trigger validation without recreating the domain (this preserves the original validation_method):

Terminal window
butterbase domains verify <domain-id>
POST /v1/{app_id}/custom-domains/{domain_id}/verify

Butterbase does not force a redirect between the apex and www. Both are just hostnames; either can be canonical.

  • Decide which one is canonical and add that one as the custom domain.
  • Redirect the other at your DNS provider’s edge (Cloudflare Redirect Rules, Netlify/Vercel DNS redirects, an ALIAS + rule at your registrar, etc.).
  • Apex on Cloudflare requires validation_method: "txt". Apex on a provider that supports CNAME flattening works with http.
  • No CNAME flattening available? Use www.example.com as the custom domain and redirect the apex to it.
MethodPathMCP domain_actionPurpose
POST/v1/{app_id}/custom-domainsaddAdd a hostname (accepts validation_method)
GET/v1/{app_id}/custom-domainslistList every hostname on the app
GET/v1/{app_id}/custom-domains/{id}/statusstatusLive verification + SSL status
POST/v1/{app_id}/custom-domains/{id}/verifyverifyRe-trigger validation
DELETE/v1/{app_id}/custom-domains/{id}removeRemove the hostname
Terminal window
butterbase domains list
butterbase domains add <hostname> [--validation-method http|txt]
butterbase domains status <domain-id>
butterbase domains verify <domain-id>
butterbase domains delete <domain-id> [--yes]
await bb.admin.domains.list();
await bb.admin.domains.add('app.example.com', 'txt');
await bb.admin.domains.getStatus(domainId);
await bb.admin.domains.verify(domainId);
await bb.admin.domains.remove(domainId);
SymptomCauseFix
403 on addApp’s org is on a tier without custom_domainUpgrade — see Plans & Usage
409 already registeredThe hostname is claimed — by this app, or another oneIf it’s this app, just check status. Otherwise pick a different hostname.
400 about the deployment backendApp is on the legacy backendMigrate the app to the WfP backend first
503 Cloudflare is not configuredSelf-hosted instance without Cloudflare credentialsConfigure Cloudflare on the control API
Error 1014 in the browserProxied (orange-cloud) CNAME across Cloudflare accountsSwitch the record to DNS-only (grey cloud)
Stuck pending on an apexUsed http on a Cloudflare-hosted zoneDelete and re-add with validation_method: "txt"
ssl_status stuck, status activeTXT validation record missing or wrongRe-read verification_records from the status endpoint and fix the record, then verify
Domain resolves but shows the old siteEdge cache after a redeployWait a few minutes and hard-refresh — see Frontend Deployment