diff --git a/apps/web/src/lib/server/hosting/hosting.test.ts b/apps/web/src/lib/server/hosting/hosting.test.ts index 2b34927..0e1b5bf 100644 --- a/apps/web/src/lib/server/hosting/hosting.test.ts +++ b/apps/web/src/lib/server/hosting/hosting.test.ts @@ -34,6 +34,7 @@ describe('normalizeSlug', () => { it('rejects reserved labels', () => { expect(() => normalizeSlug('www')).toThrow(InvalidInput); expect(() => normalizeSlug('Admin')).toThrow(InvalidInput); + expect(() => normalizeSlug('origin')).toThrow(InvalidInput); }); }); diff --git a/apps/web/src/lib/server/hosting/slugs.ts b/apps/web/src/lib/server/hosting/slugs.ts index e607ae4..5845b4e 100644 --- a/apps/web/src/lib/server/hosting/slugs.ts +++ b/apps/web/src/lib/server/hosting/slugs.ts @@ -20,6 +20,8 @@ export const RESERVED_SLUGS = new Set([ 'mooring', 'ns1', 'ns2', + // The Cloudflare for SaaS fallback origin; a routing anchor, never a site. + 'origin', 'smtp', 'staging', 'static', diff --git a/apps/web/wrangler.jsonc b/apps/web/wrangler.jsonc index 5dcfb76..e51702e 100644 --- a/apps/web/wrangler.jsonc +++ b/apps/web/wrangler.jsonc @@ -17,12 +17,14 @@ "APP_HOST": "mooring.page" }, // The apex and www are Workers custom domains (exact hostnames, DNS and - // certs managed by Cloudflare). A wildcard cannot be a custom domain, so - // subdomains bind through this route; it matches only if the zone carries - // a proxied wildcard DNS record. + // certs managed by Cloudflare). Everything else binds through this route: + // subdomains (via the zone's proxied wildcard DNS record) and Cloudflare + // for SaaS custom-hostname traffic, whose requests keep the customer's + // hostname and so match no *.mooring.page pattern. A proxied record with + // no matching route is a 522. "routes": [ { - "pattern": "*.mooring.page/*", + "pattern": "*/*", "zone_name": "mooring.page" } ], diff --git a/docs/runbooks/first-deploy.md b/docs/runbooks/first-deploy.md index 81144c3..855bc3d 100644 --- a/docs/runbooks/first-deploy.md +++ b/docs/runbooks/first-deploy.md @@ -61,9 +61,21 @@ Never in the repo, a commit, or a PR — set them with `wrangler secret put`. authored pages serve at their paths. (First served: malpercio.mooring.page, 2026-08-12.) - [ ] Enable Cloudflare for SaaS (custom hostnames) on the zone for paid - custom domains. The admin UI already records and DNS-TXT-verifies - domains; the custom-hostname API call that provisions TLS for a verified - domain is not wired yet and is the next code step once this exists. + custom domains: + - DNS record `origin` (→ `origin.mooring.page`), type `AAAA`, value + `100::`, **proxied** — an originless placeholder; with a Worker as + origin it never receives traffic, it is the routing anchor SaaS + requires. The `origin` slug is reserved in code so no tenant can + claim it. + - In SSL/TLS → Custom Hostnames, set `origin.mooring.page` as the + **fallback origin**. + - The Worker route is already `*/*` (wrangler.jsonc): custom-hostname + requests keep the customer's hostname, so they match no + `*.mooring.page` pattern — the catch-all is what routes them to the + Worker, which then classifies by the Host header. + The admin UI already records and DNS-TXT-verifies domains; the + custom-hostname API call that provisions TLS for a verified domain is + not wired yet and is the next code step once this exists. ## 5. Afterwards