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/.
→ Tenant: setting up your custom domain
Section titled “→ Tenant: setting up your custom domain”You’ll add two DNS records at your DNS provider (Cloudflare, Route 53, Namecheap, GoDaddy, etc.):
- A CNAME record that points your subdomain at Puccha’s edge.
- 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.aiTXT _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 athelp.puccha.aior 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.comwith no subdomain), use a subdomain likehelp.your-company.com. Apex / root-domain support is Enterprise-only and requires ALIAS or CNAME-flattening.
Step 3 — wait for verification
Section titled “Step 3 — wait for verification”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.
Step 4 — confirm
Section titled “Step 4 — confirm”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.
→ Tenant: when something goes wrong
Section titled “→ Tenant: when something goes wrong”Status stuck on “Verifying DNS” for hours:
- Open a terminal and run
dig CNAME help.your-company.comanddig TXT _cf-custom-hostname.your-company.com. Both must return the values you published. - If they don’t, the records haven’t propagated yet (or weren’t saved at your DNS provider). Wait the TTL or re-publish.
- 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. Samedigcheck; 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.
→ Operator: tier gating
Section titled “→ Operator: tier gating”| 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:
1. Check the row’s CF state
Section titled “1. Check the row’s CF state”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).
2. Re-poll CF directly
Section titled “2. Re-poll CF directly”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.
3. Verify DNS from a neutral resolver
Section titled “3. Verify DNS from a neutral resolver”dig @1.1.1.1 CNAME help.your-tenant.comdig @1.1.1.1 TXT _cf-custom-hostname.your-tenant.comUsing Cloudflare’s own resolver (1.1.1.1) gives the most direct picture of what their custom-hostname verifier sees.
4. CF dashboard inspection
Section titled “4. CF dashboard inspection”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.
5. Force-resend the registration
Section titled “5. Force-resend the registration”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.
→ Operator: CF token scopes
Section titled “→ Operator: CF token scopes”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.
→ Operator: erasure interaction (PDPA)
Section titled “→ Operator: erasure interaction (PDPA)”When a tenant is hard-deleted via the retention cron (ADR-0055 day-7 + ADR-0091 matrix case 5b):
eraseTenantwrites a row totenant_tombstonesBEFORE the CASCADE delete, capturing the tenant’s slug + any active custom hostnames at erasure time.- The CASCADE removes the
tenants+custom_hostnamesrows. - The retention cron then calls
purgeTenantUrlswhich enumerates every subpath + custom-domain URL the tenant had and fires CF cache purge for each. - 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_hostnamesrow, 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
(
removeCustomHostnameinpackages/edge) — this isn’t automated today; flagged for the queue-backed reindex follow-up.
→ Operator: when to escalate
Section titled “→ Operator: when to escalate”- A specific hostname is stuck on “Failed” with no clear cause
in the inline error — pull the
verification_errorfield from thecustom_hostnamesrow 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
verifyingfor > 7 days — delete the row and ask the tenant to re-add it. Long-stuck verifications rarely recover on their own.
Related code references
Section titled “Related code references”- Schema:
packages/db/migrations/0051_custom_hostnames.sql,0052_tenant_tombstones.sql - API client:
packages/edge/src/custom-hostnames.ts - Admin API:
api/tenant/custom-hostnames - Admin UI:
(admin)/(settings)/settings/custom-domains/ - Tenant resolver:
lib/tenant/resolve.ts - Erasure wiring:
lib/compliance/erase-tenant.ts - Health view:
admin/tenants/[id]/health/