Lesson 208: Construct Custom-Domain CORS Hosting Decision Receipt (2026)
Direct answer: After subdomain wasm smoke files cors_smoke_receipt_v1.json, marketing will push a custom itch domain—do not overwrite that receipt. This lesson ships cors_hosting_decision_receipt_v1.json with chosen_production_host and the split-hosting pattern (brand page on custom URL, Play on *.itch.io) before you promote HTML5 fest links.

Why this matters now (July 2026 custom domains)
July 2026 press kits ship play.yourstudio.com while engineering proved green only on studio.itch.io/demo. Construct wasm and workers fail when page origin and asset origin split without a header plan—the CORS vs subdomain playbook owns strategy; Construct subdomain preflight owns S1–S6 smoke—this lesson is the course milestone that records the hosting decision on BUILD_RECEIPT (pairs help #6 CORS fix when published, guide #9 CORP/COEP). Before October fest promotion, file Lesson 222 corp_coep_fest_receipt with three-origin map and split-hosting defer path.
Beginner path (35-minute decision)
| Step | Action | Success check |
|---|---|---|
| 1 | Confirm cors_smoke_receipt GREEN on subdomain |
S6 pass, MIME OK |
| 2 | Document three origins (page, wasm, API) | cors_origin_map_v1.json |
| 3 | Test custom domain in DevTools | Screenshot CORS lines |
| 4 | Choose hosting lane | Subdomain-only or split pattern |
| 5 | Write cors_hosting_decision_receipt_v1.json |
promotion_allowed: true |
| 6 | Pin Play URL to subdomain in press kit | No wasm on custom-only URL |
Time: ~35 minutes decision pass; 70 minutes with custom-domain experiment + facilitator README update.
Developer path (gates H1–H6)
| Gate | Check | Fail when |
|---|---|---|
| H1 | Subdomain smoke receipt exists | No cors_smoke_receipt_v1.json |
| H2 | Origin map filed | Page host ≠ wasm host undocumented |
| H3 | Custom domain tested | Marketing URL never opened in DevTools |
| H4 | Header plan or defer custom wasm | CORS errors with no mitigation |
| H5 | DevTools CORS clean on production host | Red console on chosen URL |
| H6 | Hosting receipt committed | BUILD_RECEIPT stale |
Split-hosting pattern (recommended when brand wants custom URL)
| Surface | URL | Serves |
|---|---|---|
| Marketing landing | https://play.brand.com |
Static HTML, trailer embed, press copy |
| Playable demo | https://studio.itch.io/fest-demo |
Construct wasm + workers |
| Facilitator pin | itch subdomain only | facilitator_url_canonical |
Do not point the Play button at custom domain until custom_domain_serves_wasm: true in receipt.
cors_origin_map_v1.json
{
"schema": "cors_origin_map_v1",
"page_origin": "https://play.brand.com",
"wasm_origin": "https://cdn.itch.io",
"worker_origin": "https://cdn.itch.io",
"api_origin": null,
"same_origin_for_game_assets": false
}
When same_origin_for_game_assets is false, default chosen_production_host: itch_subdomain.
cors_hosting_decision_receipt_v1.json
{
"schema": "cors_hosting_decision_receipt_v1",
"build_id": "html5-fest-2026-rc7",
"chosen_production_host": "itch_subdomain",
"custom_domain_enabled": true,
"custom_domain_serves_wasm": false,
"split_hosting_pattern": true,
"paired_cors_smoke_receipt": "release-evidence/html5/cors-smoke/CORS_SMOKE_RECEIPT.json",
"gates": {
"H1_subdomain_smoke": "pass",
"H2_origin_map": "pass",
"H3_custom_tested": "pass",
"H4_defer_or_header_plan": "pass",
"H5_devtools_clean_on_production": "pass",
"H6_receipt": "pass"
},
"facilitator_url_canonical": "https://studio.itch.io/fest-demo",
"promotion_allowed": true
}
Never set custom_domain_tested: true inside cors_smoke_receipt—keep smoke and hosting receipts separate files.
Publish gate
ALTER TABLE release_publish_gate ADD COLUMN IF NOT EXISTS
html5_hosting_decision_blocked BOOLEAN NOT NULL DEFAULT false;
CI verify_cors_hosting_decision_v1 requires cors_hosting_decision_receipt_v1.json when html5_host column is custom or split.
Wire into BUILD_RECEIPT
| Column | Example |
|---|---|
html5_host |
itch_subdomain or split |
cors_smoke_receipt |
path to smoke JSON |
cors_hosting_decision_receipt |
path to hosting JSON |
facilitator_url_canonical |
itch subdomain URL only |
Thursday row review must show both receipt paths.
Prerequisites
- Lesson 201 — itch_public scope
- Subdomain evening tutorial
- Construct CORS playbook
- itch MIME help
Common mistakes
- Overwriting
cors_smoke_receiptwhen custom domain fails—hosting decision is a second file. - Sharing custom URL in Discord while wasm only works on subdomain.
- Fixing MIME repeatedly while origin stays split.
- Enabling custom domain before S6 subdomain smoke passes.
Troubleshooting
| Symptom | Lane |
|---|---|
| Subdomain OK, custom blank | Expected—use split pattern or defer custom wasm |
| CORS on wasm only | H2 origin map + playbook |
| MIME octet-stream | itch MIME help first |
| Triple-channel label red | Lesson 201 |
Mini exercise (50 minutes)
- Pass subdomain smoke; archive
cors_smoke_receipt. - Open custom domain; capture CORS console screenshot.
- File origin map showing split hosts.
- Choose split pattern; write hosting receipt.
- Update press kit Play link to itch subdomain only.
Continuity
- Previous: Lesson 207 — Whisper path when concat fails
- Next: Lesson 209 — cross-engine localization matrix
- Guides: Construct subdomain smoke, Custom domain CORP/COEP preflight
- Help (planned): Construct custom-domain CORS fix (#6)
FAQ
Can we ship custom domain if marketing insists?
Only with custom_domain_serves_wasm: true and H5 green on that URL—otherwise use split pattern.
Does this replace the playbook?
No—playbook is policy; this lesson is receipt wiring in your RPG live-ops course.
Godot HTML5 too?
Same origin discipline—see Godot WASM memory playbook for a different failure family.
Subdomain smoke proves the build works—hosting decision receipt proves which URL you are allowed to put in the fest press kit.