Attaching a custom domain to a managed blog now has a second required step that nothing enforces, and skipping it breaks that tenant's newsletter signup in a way we cannot see.
Why
Since #1579 the managed-blog signup form renders a Cloudflare Turnstile widget, and the relay refuses an anonymous subscribe without a valid token. A Turnstile sitekey is bound to a hostname list.
Subdomains are covered automatically — a hostname entry covers that hostname and all of its subdomains — so every *.blogs.ecency.com tenant works off the existing ecency.com entry and needs nothing. A custom domain on its own apex does not.
blog.hivexplorer.com was added manually on 2026-08-20; the widget now carries ["blog.hivexplorer.com", "ecency.com"]. The next custom domain will not be, unless someone remembers.
The failure mode is the problem
The widget fails to solve, the submit button never enables, and the reader sees a form that quietly does nothing — on someone else's blog. No error reaches us. sync-custom-domains.py issues the certificate and writes the vhost every 5 minutes without touching Turnstile, so the blog itself looks entirely healthy.
What is in place
apps/self-hosted/hosting/origin/README.md documents the step next to the nginx one, including that the update is a PUT of the whole widget object — the domains array must be read and appended to, or every previously registered tenant is silently dropped. The token needs Turnstile: Edit; with read-only the API answers 10405/10000 without naming the missing permission.
Options
- Extend
sync-custom-domains.py to reconcile the Turnstile domain list the same way it reconciles vhosts — it already reads verified custom domains from the hosting DB, so it has the list.
- Have the hosting API append the hostname when a custom domain is verified.
- Leave it manual and add a check that alerts when a served custom domain is absent from the widget.
(1) fits the existing shape best: the sync already owns "make the origin match the DB", and this is the same reconciliation against a different API. It needs a Cloudflare token with Turnstile: Edit on the origin box, which is the main new consideration.
Until then this is a documented manual step, which is exactly the kind that gets missed.
Attaching a custom domain to a managed blog now has a second required step that nothing enforces, and skipping it breaks that tenant's newsletter signup in a way we cannot see.
Why
Since #1579 the managed-blog signup form renders a Cloudflare Turnstile widget, and the relay refuses an anonymous subscribe without a valid token. A Turnstile sitekey is bound to a hostname list.
Subdomains are covered automatically — a hostname entry covers that hostname and all of its subdomains — so every
*.blogs.ecency.comtenant works off the existingecency.comentry and needs nothing. A custom domain on its own apex does not.blog.hivexplorer.comwas added manually on 2026-08-20; the widget now carries["blog.hivexplorer.com", "ecency.com"]. The next custom domain will not be, unless someone remembers.The failure mode is the problem
The widget fails to solve, the submit button never enables, and the reader sees a form that quietly does nothing — on someone else's blog. No error reaches us.
sync-custom-domains.pyissues the certificate and writes the vhost every 5 minutes without touching Turnstile, so the blog itself looks entirely healthy.What is in place
apps/self-hosted/hosting/origin/README.mddocuments the step next to the nginx one, including that the update is a PUT of the whole widget object — thedomainsarray must be read and appended to, or every previously registered tenant is silently dropped. The token needsTurnstile: Edit; with read-only the API answers10405/10000without naming the missing permission.Options
sync-custom-domains.pyto reconcile the Turnstile domain list the same way it reconciles vhosts — it already reads verified custom domains from the hosting DB, so it has the list.(1) fits the existing shape best: the sync already owns "make the origin match the DB", and this is the same reconciliation against a different API. It needs a Cloudflare token with
Turnstile: Editon the origin box, which is the main new consideration.Until then this is a documented manual step, which is exactly the kind that gets missed.