ADR-0009: The web platform: one player, bundles, a separate play origin
- Status: Proposed
- Date: 2026-09-30
Context
The site should let people browse and play games, chat with the Director, manage projects and have accounts. Findings (research/web-platform.md):
- A 4.7.2 web player is 38.8 MB raw and 9.57 MB as Vercel serves it (brotli, measured).
- Threaded builds need cross-origin isolation of the whole frame chain, and
credentiallessCOEP doesn't work in Safari or iOS Safari. Godot has defaulted to single-threaded web exports since 4.3. - Vercel Blob can't serve HTML pages or COOP/COEP headers, but it serves
ACAO: *, so the engine can fetch from it. A same-origin rewrite keeps our URLs. - User Godot content can execute code:
JavaScriptBridge.evalon the web. - The
xaxis-projectsVercel team is on the Pro plan (checked through the API on 2026-09-30).
Decision
- Stack. A Next.js 16 App Router site in
web/(a pnpm workspace package), deployed as the Vercel projectgamewrightwith Root Directoryweb. Later: AI SDK for chat streaming, Better Auth on Neon Postgres via Drizzle, Vercel Blob for bundles, Vercel Workflows for long runs, Vercel Sandbox with a Godot image for server-side verification. - Play = shared player + bundle. One no-threads web player per engine release, served
immutable under
/engine/<version>-<hash>/. Bundles live under/g/<gameId>/<bundleHash>/. A game page never needs Godot on a server. - A separate registrable domain for play. Games run in an iframe on a play origin that is not
a subdomain of the site, with
sandbox="allow-scripts allow-same-origin allow-pointer-lock", a strict CSP includingframe-ancestors, no cookies, and origin-checkedpostMessage. - No threads by default. A threaded build is opt-in, per game, opened top-level on the play origin under a path that sends COOP/COEP.
- Phases. Landing page → gallery of playable demos → Director preview (invite, bring your own key, hosted MCP) → accounts and projects → community (publish, remix, moderation).
- Deploys are proven. Every deploy script ends by fetching the live URL, and for play pages booting the game in headless Chromium.
Alternatives considered
- Per-game Godot exports on the server. 45 MB per game, Godot on the hot path, and a format that carries code.
- Games on the site origin or a subdomain. A subdomain is same-site: cookies and CSRF defences don't separate it.
- A static HTML landing page (Reelwright's). It's smaller today, but it would be thrown away at the gallery phase. The Next skeleton proves the monorepo and Root Directory pipeline now.
- Cloudflare R2 as primary storage. A second vendor, and a 25 MiB static-asset cap that the wasm exceeds. Kept as the bandwidth escape hatch.
Consequences
- The site is cheap to run until the Director chat opens.
- Two domains are needed. The site's is gamewright.diy (bought 2026-09-30, Namecheap DNS,
attached to the Vercel project with
wwwredirecting to the apex). The play origin's separate registrable domain is still the owner's decision, needed before user-made games are hosted. - The play origin, the player export and the web ladder rung have to be in place before any user-made game is hosted.
Notes
- 2026-09-30: the play origin is live on its vercel.app name. Waiting for a play domain would
have blocked M5, so the play origin is its own Vercel project,
gamewright-playonxaxis-projects, served at its production domain https://gamewright-play.vercel.app and deployed and proven bytools/play-origin/deploy.sh.vercel.appis on the Public Suffix List (private section, "Vercel, Inc", list version 2026-09-24_13-26-36_UTC, fetched from publicsuffix.org the same day), sogamewright-play.vercel.appis itself a registrable domain: a different site fromgamewright.diy, and fromgamewright.vercel.apptoo. That is what decision 3 asks for. Moving to a custom play domain later is configuration, not code: add the domain to thegamewright-playproject and run the deploy, which prefers a custom production domain over the vercel.app name when it writes the site's game list (web/data/games.json). The site's origins inplay/vercel.json'sframe-ancestorsstay as they are. - 2026-09-30: bundles ship inside the play deployment, not in Blob yet. Vercel's limits (docs
updated 2026-09-16) cap a Pro CLI deployment at 1 GB of source and 15,000 files, with no
per-file cap on static files; the 10 MB cacheable-response cap applies to Function responses.
Measured on the live origin: the 39,514,754-byte wasm is served as a static file, brotli, at
9,550,647 bytes on the wire,
application/wasm, immutable, andx-vercel-cache: HITon a repeat fetch. Blob needs a store and a token; it waits for user-made games.