v0.94.0 — Dev environment rename: next.purplecat.games
Shipped 2026-09-19 — merged to main as 06f65ff9, tagged v0.94.0. Built on version/0.94.0, six commits. Not one of the releases
ROADMAP.md sequences from the heuristic evaluation;
slotted ahead of them, which moved every planned release down one number
(Roles and the Gatekeeper is v0.95.0, and so on through v0.105.0). The roadmap
page is dev-environment-rename.
dev.purplecat.games is now next.purplecat.games, and api-dev is
api-next, so that the dev name is free for something else. Nothing about
the environment other than its hostnames changed: it is still named dev in
every resource — cluster, bucket, task family, secret, image tag, workflow —
because the infrastructure module takes environment and app_subdomain
separately, and changing the first would have recreated a bucket and a cluster
to alter a label nobody types into a browser.
Why it was not a DNS change
The ask was "can I just change the Route 53 entry?", and the answer was no, four times over. The hostname is derived once in Terraform and used in four places a DNS edit does not reach:
- The CloudFront alias. A distribution answers 403 to any host it does not list, before the origin is consulted.
FRONTEND_URLin the task definition, which the server turns into its CORS allowlist (since v0.56.x) and into every campaign and character invite link. A SPA on an unlisted origin has every request and the socket handshake rejected — with a console indistinguishable from a dead server.- The assets bucket's CORS rule, which presigned browser uploads depend on, independently of the API's.
- The ALB's host-header rule for the API.
And two in CI: the deploy workflow finds the distribution to invalidate by
alias, so a stale APP_HOST is a deploy that silently serves the previous
bundle; and API_BASE_URL is baked into the bundle at build time.
What changed
- N.1 — Terraform.
module.dev:app_subdomain = "next",api_subdomain = "api-next". The plan was the contract: 3 to add, 3 to change, 3 to destroy — alias, assets CORS rule and listener-rule condition in place; the two Route 53 records replaced because their name is their identity; a task-definition revision whose only real change wasFRONTEND_URL. Applied 2026-09-19;terraform planis "No changes" after. - N.2 — Deploy Dev, and a guard.
APP_HOST/API_BASE_URLfollow, anddeployHostParity.test.ts(server-core, beside the other CI-file guards) reads both sides — Terraform's subdomains fordevandalpha, each workflow's two variables — and fails when they disagree. It also pins that the invalidation step still keys on the alias, which is why the parity matters. - N.3 — The mentions in
cors.tsand its test. - N.4 — The cutover. Apply, then Deploy Dev — not apply-then-roll: the
bundle in the bucket had
api-devbaked in and had to be rebuilt, and the workflow derives its task definition from the family's latest revision, which after the apply was Terraform's. Verification is below.
Verification
Run 2026-09-19 after terraform apply -target=module.dev and Deploy Dev run
35474803633 (all four jobs green; service rolled to task-definition revision 9,
rolloutState: COMPLETED; invalidation ICTRB3YM0R0UZPJ5DO82HSJ92Y on
E3OOWUJUTXE95K found by alias and Completed — no ::warning::).
| Breakage | Check | Result |
|---|---|---|
| CloudFront alias | curl -sI https://next.purplecat.games |
HTTP/2 200, served by CloudFront; the bundle it serves names api-next three times and api-dev zero |
| API CORS | OPTIONS /api/campaigns with Origin: https://next.purplecat.games |
204, access-control-allow-origin: https://next.purplecat.games, credentials true; a GET answers 401 with the header (auth, not CORS); the Socket.IO polling handshake 200 with the header; Origin: https://evil.example gets no header |
| Assets bucket CORS | S3 preflight for PUT from the new origin — what the browser sends before a presigned upload |
200, Access-Control-Allow-Origin: https://next.purplecat.games, methods POST, GET, PUT; the same from the old origin is 403 |
| ALB host rule | https://api-next.purplecat.games/health |
200 |
| Sign-in | Load https://next.purplecat.games/ in a browser |
The SPA boots, Clerk initialises and hands off to its hosted sign-in (right-ibex-13.accounts.dev, a development instance — so there is no per-host allowed-origins list to update; the page's worry about the production instance was wrong). Verified up to the credential form; credentials are not something the release process types |
| Old hosts | dig against the zone's own nameserver |
api-dev.purplecat.games → NXDOMAIN. dev.purplecat.games already answers again — a plain A record to 75.58.11.253, created outside Terraform between the apply and this check, which is the reuse the rename was for |
| Terraform | terraform plan -target=module.dev after the apply |
No changes |
Found on the way
- Deploy Dev could not start. Its
bundle-modulesjob checks the platform out underplatform/so the module repos can sit beside it, andpnpm/action-setup@v4readspackage.jsonfrom the workspace root by default — so the first run after the push failed at "Setup pnpm" with No pnpm version is specified, before anything was built. The same workflow had passed on 2026-09-09; thev4tag floats and that run's logs have expired, so what changed underneath is not recorded. The step now namespackage_json_file: platform/package.json, andciActionVersions.test.tsfails any job whose checkout has apath:and whose pnpm setup does not point at it. scripts/is still not a workspace onmain. The page first put the guard there; the v0.84.0 branch that made that directory a workspace was never merged, so a test there does not run. The guard lives in server-core.