Bake the R2-subdomain pattern into gfcx_subdomain_new scaffold — bucket + custom-domain + Worker + holding page + redirects + HEAD-check audit as a single mint

DARE.CO.UK · PARKED SKETCH · 2026-06-07

Mirrored from ~/.claude/.../memory/parked_sketch_gfcx_r2_subdomain_scaffold_2026-06-06.md. This is a design sketch parked for future build — read for context, not as a current deliverable.

Dan 2026-06-06 after shipping media.gf.cx with bucket, custom-domain, holding page, Worker (index rewrite + redirects + custom 404), HEAD-check audit — “These should be written into the pattern for r2-sub-domain setup site.config.” Today’s media.gf.cx was assembled step-by-step. The full stack of decisions + commands is now known; next time a new R2-backed gf.cx subdomain is needed it should mint via scaffold, not bespoke.


Sketch: extend ~/bin/gfcx_subdomain_new.py (or create a sister gfcx_r2_subdomain_new.py) that mints an R2-backed gf.cx subdomain end-to-end. One command produces: R2 bucket, Worker repo scaffold, holding page, custom 404, HEAD-check audit script wired in.

Why (Dan 2026-06-06):

“These should be written into the pattern for r2-sub-domain setup site.config”

Today’s media.gf.cx rollout was assembled step by step: 1. Pick subdomain (media.gf.cx chosen over assets.gf.cx because the latter was taken) 2. Create R2 bucket (gf-cx-media, scope-first naming) 3. Bind custom domain via wrangler (needed zone-id lookup) 4. Upload holding page to bucket root 5. Discover bare / 404s (R2 doesn’t auto-serve index.html) 6. Build CDN, security layer, and DNS provider sitting in front of dare.co.uk." data-tip="Cloudflare — the CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Worker fronting the bucket — trailing-slash rewrite + redirects table + custom 404 7. Remove R2 direct-domain binding (conflict with Worker route) 8. Deploy Worker (registers route) 9. Verify all three layers (200 holding, 200 asset, 404 custom) 10. Wire HEAD-check audit script (gfcx_media_link_check.py)

Each step is now known. Next time a new R2-backed gf.cx subdomain is needed (e.g. evidence.gf.cx for pa.gf.cx evidence assets, archive.gf.cx for vault archives) it should mint via scaffold, not bespoke. This is the same logic that produced gfcx_subdomain_new.py for CDN, security layer, and DNS provider sitting in front of dare.co.uk." data-tip="Cloudflare — the CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Pages subdomains.

Scaffold inputs (proposed):

gfcx_r2_subdomain_new.py <subdomain> [--surfaces happiness,pa]
# Example:
gfcx_r2_subdomain_new.py media.gf.cx --surfaces happiness

Scaffold outputs:

  1. R2 bucket created via wrangler r2 bucket create gf-cx-<type> (where <type> is derived from subdomain prefix). Bucket name follows scope-first convention.
  2. Worker repo scaffolded at ~/Code/gf-cx-<type>-worker/: - wrangler.toml with route + R2 binding pre-filled - src/index.js with the three-layer logic (trailing-slash → index.html, _redirects table, custom 404) - src/_redirects.json empty seed - holding.html (uploaded to bucket root) - 404.html template (compiled into the Worker)
  3. Custom-domain bind via wrangler r2 bucket domain add with auto-fetched zone-id
  4. Holding-page upload via wrangler r2 object put
  5. R2 direct-domain removed before Worker deploy (avoid the hostname-conflict gotcha)
  6. Worker deployed via wrangler deploy (registers the route)
  7. HEAD-check audit entry added — extend ~/bin/_gfcx_link_audit.py (see feedback_head_check_audit_pattern_generalise_at_3rd_use_2026-06-06) with the new subdomain + a sample HTML root to scan
  8. Verify all three paths return expected codes (200 holding, 200 known asset, 404 missing) — abort if any fails
  9. Run ~/bin/gfcx_dns_unstick.sh <subdomain> to flush the three local cache layers (macOS mDNSResponder + NextDNS Cache Boost + Tailscale MagicDNS). Without this, NextDNS serves a stale negative-cache for the new subdomain and the household network can’t reach it for ~15-60 min. Dan flagged this 2026-06-06 PM: “every new sub-domain gets rejected by nextDNS, you have a custom script to release it.” Memo: reference_gfcx_dns_unstick_canonical_2026-06-06.

Four things the scaffold MUST encode (lessons from today):

  1. Don’t blindly clone existing-bucket+R2-custom-domain flow — the hostname conflicts with the Worker route. Order is: remove direct R2 domain, then deploy Worker with route.
  2. wrangler r2 bucket domain remove deletes the DNS record at the gf.cx zone. Worker routes do NOT auto-create DNS records — they only intercept traffic already proxied through CDN, security layer, and DNS provider sitting in front of dare.co.uk." data-tip="Cloudflare — the CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF. After removing the R2-direct binding, manually POST a proxied DNS record before/after Worker deploy: bash CF_TOKEN=$(op read "op://Code Shared/Cloudflare gf.cx full access/credential") curl -s -X POST "https://api.cloudflare.com/client/v4/zones/<zone-id>/dns_records" \ -H "Authorization: Bearer $CF_TOKEN" -H "Content-Type: application/json" \ -d '{"type":"AAAA","name":"<subdomain>","content":"100::","ttl":1,"proxied":true,"comment":"gf-cx-<type>-worker route placeholder"}' (AAAA with 100:: content is the canonical CDN, security layer, and DNS provider sitting in front of dare.co.uk." data-tip="Cloudflare — the CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF placeholder for proxied-only hostnames where the actual destination is a Worker route. Why 100:: specifically: RFC 6666 reserves 100::/64 as the IPv6 discard-only prefix — packets to that address get black-holed, so even if CDN, security layer, and DNS provider sitting in front of dare.co.uk." data-tip="Cloudflare — the CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF’s proxy layer ever drops, there’s zero risk of leaking real traffic to a real host. CDN, security layer, and DNS provider sitting in front of dare.co.uk." data-tip="Cloudflare — the CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF docs recommend this exact placeholder. Dan 2026-06-06: “ip.6 is a smart way to go with AAAA records.” Don’t use IPv4 placeholders like 192.0.2.1 — they’re routable in principle, so the failure mode is dirtier.)
  3. Scope-first bucket naming. gf-cx-<type> not <type>-gf-cx. Validated 2026-06-06 against existing bucket list.
  4. Path shape <surface>/<asset-type>/<file>. Multi-surface bucket, scoped paths. Documented in the holding page automatically.

Sister memories this composes with:

Trigger to build: next time a gf.cx surface needs its own R2 subdomain. Today the candidate queue includes archive.gf.cx (vault-archives bucket), evidence.gf.cx (pa.gf.cx evidence buckets), and possibly snapshots-r2.gf.cx (separate snapshots store). Don’t build until there’s a concrete second use — premature scaffolding locks in a contract that doesn’t yet fit a second case.

Not built today because: today’s was the first R2-subdomain rollout end-to-end. The shape is now visible but unproven against a second use. By the rule in feedback_head_check_audit_pattern_generalise_at_3rd_use_2026-06-06: 1st = inline, 2nd = copy-adapt, 3rd = extract. We’re at 1.

Source: parked_sketch_gfcx_r2_subdomain_scaffold_2026-06-06.md · Rendered 2026-06-07 06:30