Three hours from git checkout -b to live: cf-saas-decommission
shipped end-to-end on production. Caddy GCE VM stopped, customer
hostnames now terminate TLS at Cloudflare edge with auto-issued
Google CA certs, and the dispatcher worker (already serving
*.vibehost.space) gained a custom-domain branch with
Hard‑Rule‑#8 access‑gate enforcement.
Two real customer hostnames — lacoste.projectd.cc and
cpbl.ladykaren.org — now serve real content (they previously
404'd because the API middleware was never wired in prod). Plus
a fresh-bind E2E proves the same flow works for new prod users:
add hostname → set 1 CNAME + 1 TXT → click verify → cert active in ~1 min.
The two existing prod custom domains were silently broken — Caddy was on the path but the API custom-domain-proxy middleware was never enabled in production (CUSTOM_DOMAIN_PROXY_ENABLED defaulted to false). After cutover, requests terminate at CF edge with an auto-issued cert and route through the dispatcher Worker.
$ curl -sI https://lacoste.projectd.cc/ HTTP/2 404 via: 2.0 Caddy via: 1.1 google x-powered-by: Express content-type: text/html $ curl -s ... | head -3 <!DOCTYPE html> <pre>Cannot GET /</pre>
Customer DNS pointed at cname.vibehost.host → Caddy GCE IPs → API pod → Express default not-found. The customer thought their site was up because the dashboard showed verified.
$ curl -sI https://lacoste.projectd.cc/ HTTP/2 200 server: cloudflare cf-ray: 9f97af3cccc6e60e-SIN strict-transport-security: max-age=31536000 $ openssl s_client ... subject=CN = lacoste.projectd.cc issuer=Google Trust Services, CN = WE1 notAfter=Aug 8 04:19:57 2026 GMT
CF edge terminates TLS with a cert auto-issued by Google CA. Dispatcher worker reads custom-domain:<host> from KV → resolves to the app's tenant subdomain → /authz/check AND-composes visibility / password / share-link gates → R2 serves customer content.
404 Cannot GET / from broken Caddy、現在是真的 LACOSTE / CPBL 內容 from CF + dispatcher worker."Most of the migration value comes from new domains, not the 2 backfilled ones. So I bound a fresh hostname end-to-end via the live API to prove the experience.
vibehost domain add cf-saas-brief cf-saas-test.vibehost.cc
API returns DNS instructions — 1 CNAME (apex → cname.vibehost.host) + 1 ownership TXT.
CNAME + _vibehost.<host> TXT in their DNS provider.
For this E2E, vibehost.cc (testing zone) — the same flow on any customer-owned zone.
vibehost domain verify cf-saas-brief cf-saas-test.vibehost.cc
API checks DNS, marks verifiedAt, calls cf.create(), writes KV (custom-domain mapping + ownership token), tags row cf_sync_state='ok'.
/.well-known/cf-custom-hostname-challenge/<id> from KV.
https://cf-saas-test.vibehost.cc/ → HTTP/2 200 with real app content.
Same edge / cert pipeline as vibehost.com. Caddy entirely out of the picture.
BEFORE
customer DNS: blog.example.com CNAME → cname.vibehost.host
│
cname.vibehost.host A 35.187.148.54 (Caddy GCE)
A 34.81.53.121 (Caddy GCE)
│
▼
┌─────────────────────┐
│ Caddy on-demand TLS │
│ (Let's Encrypt) │
└──────────┬──────────┘
│ X-Forwarded-Host
▼
┌─────────────────────┐
│ apps/api (GKE) │
│ middleware │ ← CUSTOM_DOMAIN_PROXY_ENABLED
│ never wired │ defaulted false in prod
└──────────┬──────────┘
▼
404 Cannot GET /
AFTER (shipped)
customer DNS: blog.example.com CNAME → cname.vibehost.host (unchanged)
│
cname.vibehost.host CNAME → cf-saas-origin.vibehost.com proxied
│
▼
┌─────────────────────────┐
│ Cloudflare edge │
│ TLS terminate (Google CA)│
│ CF for SaaS fallback │
└─────────────┬───────────┘
│ per-host worker route
▼
┌─────────────────────────┐
│ apps/dispatcher Worker │
│ KV custom-domain:<host> │
│ → tenant subdomain │
│ /authz/check (3 gates) │
└─────────────┬───────────┘
▼
R2 / SSR Worker
200 — real customer content0019_custom_domains_cf_columns.sql applied to prod Postgres — 4 columns + partial indexCLOUDFLARE_DISPATCH_KV_ID already wired via Pulumi configMap; new prod deploy picked it up automatically--dry-run then --confirm) — 2 rows registered with CF, 2 unverified soft-deletedvibehost.com zone → cf-saas-origin.vibehost.comvibehost.com zone (*/* wildcard rejected by API token, only precise patterns)cf-saas-origin.vibehost.com — the cutover momentgcloud compute instances stop — TERMINATED, disk preserved for emergency rollback