Skip to content

Help-center custom domain — DNS, TLS, troubleshooting

Operational runbook for the help-center custom-domain feature (ADR-0091 Phase 2). Two audiences in one document:

  • Tenant-facing instructions — the sections marked with → Tenant below can be lifted verbatim into the admin UI help text, the public docs site (when one exists), or pasted into a support reply.
  • Operator troubleshooting — the → Operator sections are for Puccha staff investigating a stuck verification or a TLS failure.

The CF Custom Hostnames API integration lives in packages/edge/src/custom-hostnames.ts. The admin UI is at (admin)/(settings)/settings/custom-domains/.


You’ll add two DNS records at your DNS provider (Cloudflare, Route 53, Namecheap, GoDaddy, etc.):

  1. A CNAME record that points your subdomain at Puccha’s edge.
  2. A TXT record that proves you own the domain.

After both records are live, Cloudflare issues a TLS certificate (takes a few minutes to a few hours depending on your DNS provider’s TTL). Once active, your help center loads at your own domain with HTTPS.

Step 1 — register the hostname in Puccha

Section titled “Step 1 — register the hostname in Puccha”

In your admin app, go to Settings → Custom domains and click Add hostname. Enter the full subdomain you want to use, e.g. help.your-company.com. Click Add hostname.

Puccha shows you two DNS record values to publish:

CNAME help.your-company.com → tenants.puccha.ai
TXT _cf-custom-hostname.your-company.com → <a long random string>

The TXT name and value are generated by Cloudflare per hostname. Copy them exactly — extra spaces or trailing dots will break verification.

Step 2 — add the records at your DNS provider

Section titled “Step 2 — add the records at your DNS provider”

Open your DNS provider’s control panel and add both records as shown.

Notes:

  • The CNAME target is always tenants.puccha.ai — do not point at help.puccha.ai or any tenant-specific hostname.
  • Some DNS providers strip the trailing dot on the CNAME target; that’s fine. They should not strip on the TXT value.
  • If your DNS provider doesn’t support a CNAME at the exact host you wanted (most don’t allow CNAME at the apex / root, e.g. your-company.com with no subdomain), use a subdomain like help.your-company.com. Apex / root-domain support is Enterprise-only and requires ALIAS or CNAME-flattening.

Back in Settings → Custom domains, click Check status on your hostname row. The status moves through:

Status What it means
Pending Row created; CF hasn’t called out yet. Usually flips to Verifying within seconds.
Verifying DNS CF is polling your DNS records. Until both CNAME and TXT propagate, this is the expected state.
Active TLS issued, hostname live. Visitors can hit https://help.your-company.com.
Failed DNS verification failed or TLS issuance hit an error. See the inline message for the specific cause.

Typical verification takes 15 minutes to 4 hours, dominated by DNS TTL propagation. If you set a short TTL (e.g., 300 seconds) before publishing the records, it speeds up.

Once status is Active, visit https://help.your-company.com. You should see your help center at your brand. Try a few articles to confirm content + the chat widget load.


Status stuck on “Verifying DNS” for hours:

  1. Open a terminal and run dig CNAME help.your-company.com and dig TXT _cf-custom-hostname.your-company.com. Both must return the values you published.
  2. If they don’t, the records haven’t propagated yet (or weren’t saved at your DNS provider). Wait the TTL or re-publish.
  3. If they do return the right values and Puccha still says “Verifying” after 4 hours, click Remove and Add hostname again — re-issuing the CF registration is faster than waiting on a stuck verification.

Status went to “Failed”:

The inline message (shown under the hostname) names the cause. Common ones:

  • dns not propagated — DNS records aren’t visible from CF’s resolvers. Same dig check; usually waiting 15 more minutes fixes it.
  • txt record mismatch — the TXT value you published doesn’t match the value Puccha shows. Remove the TXT, re-copy from Puccha, re-publish.
  • hostname already registered elsewhere — that exact hostname is registered to a different Cloudflare zone, or a different Puccha tenant. Contact support.

TLS works but the page 404s:

The custom domain is verified but no article + no page matches the URL. Check the Documents page — at least one document must have Public surfaces: Help center ticked for the index to show anything. Until you publish a doc + check that box, the help center exists but has no content.

Custom domain stopped working after I downgraded:

That’s expected and reversible. When you downgrade below Business, your custom domain enters a permanent-301 mode: visitors hitting help.your-company.com/article get redirected to help.puccha.ai/{your-slug}/article automatically. The brand is gone but the SEO is preserved. To bring the custom domain back, just re-upgrade — the same hostname row reactivates.


Plan Max active custom hostnames Enforcement
Free, Team 0 (feature unavailable) POST /api/tenant/custom-hostnames returns 403
Business 1 quota check in the same POST
Growth 3 quota check
Enterprise unlimited (subject to CF account cap of 5000) quota check

The cap is computed against status='active' rows only. Rows that went to inactive (post-downgrade) don’t count toward the cap.


→ Operator: troubleshooting a stuck verification

Section titled “→ Operator: troubleshooting a stuck verification”

When a tenant says “I added the records, it’s been hours, still not active,” walk through:

In the admin tenant-health view at /admin/tenants/{id}/health, the custom-hostnames panel shows the status + hours-stuck. Anything in verifying for > 24h is flagged stale (amber alert at the top of the panel).

The admin UI’s Check status button calls POST /api/tenant/custom-hostnames/{id} which in turn re-fetches the CF state and updates the row. If status doesn’t change after a fresh poll, CF still hasn’t verified DNS.

Terminal window
dig @1.1.1.1 CNAME help.your-tenant.com
dig @1.1.1.1 TXT _cf-custom-hostname.your-tenant.com

Using Cloudflare’s own resolver (1.1.1.1) gives the most direct picture of what their custom-hostname verifier sees.

If you have CF dashboard access, navigate to the help zone → SSL/TLS → Custom Hostnames and find the hostname by its cf_hostname_id (stored in the custom_hostnames row). The dashboard shows the verification status plus any TLS issuance errors with more detail than the API returns.

If the row’s cf_hostname_id is empty (registration never succeeded), the easiest path is to delete the row from the admin UI and re-add it. The new POST will re-call the CF API. Original ID is gone; tenant has to re-publish the records but their existing DNS continues to work for the new registration.


Two API tokens are configured per environment, with least-privilege separation (ADR-0091 Phase 2 token split):

Env var CF scope Used by
CF_HOSTNAME_TOKEN Zone.SSL and Certificates + Zone.Custom Hostnames POST /api/tenant/custom-hostnames, status polling, deletion
CF_CACHE_PURGE_TOKEN Zone.Cache Purge Help-center cache invalidation in the erasure cron (process-due-erasures.ts)

Both target the same zone (CF_HELP_ZONE_ID — the help.puccha.ai zone). A leaked cache-purge token can’t manage hostnames, and vice versa.

During the migration window, code reads CF_HOSTNAME_TOKEN ?? CF_CACHE_PURGE_TOKEN — production should set both explicitly. Update wrangler.toml + Secrets for each env when wiring a new deployment.


When a tenant is hard-deleted via the retention cron (ADR-0055 day-7 + ADR-0091 matrix case 5b):

  1. eraseTenant writes a row to tenant_tombstones BEFORE the CASCADE delete, capturing the tenant’s slug + any active custom hostnames at erasure time.
  2. The CASCADE removes the tenants + custom_hostnames rows.
  3. The retention cron then calls purgeTenantUrls which enumerates every subpath + custom-domain URL the tenant had and fires CF cache purge for each.
  4. Subsequent visitor requests resolve via the help host → tombstone lookup → 410 Gone with Cache-Control: no-store.

Custom hostnames specifically:

  • The custom_hostnames row is gone (CASCADE), so the CF Custom Hostnames registration is not auto-deleted on the CF side by our code. The TLS cert keeps serving (CF doesn’t know the tenant is gone). Visitors hit our Worker via the CF edge, then our resolver finds no custom_hostnames row, checks the tombstone hostnames array via JSON1, and emits 410.
  • After ~30 days of 410 responses, search engines will deindex. At that point an operator can DELETE the CF registration (removeCustomHostname in packages/edge) — this isn’t automated today; flagged for the queue-backed reindex follow-up.

  • A specific hostname is stuck on “Failed” with no clear cause in the inline error — pull the verification_error field from the custom_hostnames row directly, then check CF dashboard for the full error chain. CF support ticket if the issue is on their side.
  • TLS issuance succeeded but visitors hit certificate warnings — verify the visitor’s browser isn’t intercepting via a corporate proxy; verify the CF-issued cert chain via openssl s_client -connect help.tenant.com:443 -servername help.tenant.com.
  • Hostname stuck on verifying for > 7 days — delete the row and ask the tenant to re-add it. Long-stuck verifications rarely recover on their own.