Architecture
Containers viewβ
Repository layoutβ
/
βββ back/ β PartyKit backend (Durable Object)
β βββ game.ts β thin dispatcher β routes messages to handlers
β βββ game-context.ts β GameContext interface
β βββ types.ts β exported types & interfaces
β βββ constants.ts β word lists (25Γ25), category prompts
β βββ utils.ts β pure utilities: generateIdentity(), shuffle()
β βββ scoring.ts β pure scoring functions
β βββ question.ts β buildQuestionPayload, broadcast helpers
β βββ persistence.ts β saveState, upsertKvSession, endGame
β βββ handlers/
β βββ session.ts β create_session, rejoin_host, join_session, rejoin_session
β βββ game-flow.ts β host control handlers + submit_answer
β βββ timer.ts β startTimer, handleTimerExpiry
βββ front/ β Astro + React frontend
β βββ src/
β βββ lib/
β β βββ socket.ts β PartySocket factory (createSocket(room))
β β βββ rejoin.ts β shared localStorage credential helpers
β βββ types/events.ts β shared event payload types
β βββ stores/
β β βββ hostStore.ts
β β βββ participantStore.ts
β βββ hooks/
β β βββ useHostSocket.ts
β β βββ useParticipantSocket.ts
β β βββ useUrlSync.ts β URL β store sync for host & participant
β βββ components/
β βββ host/
β βββ participant/
βββ docs-site/ β Docusaurus docs
βββ proxy-worker/
β βββ index.ts β Cloudflare Worker: routes /docs/* to docs Pages
βββ wine-answers-worker/ β Cloudflare Worker: curated wine answer suggestions (KV-backed)
β βββ index.ts β GET/POST/DELETE endpoints for answer collections
βββ partykit.json β PartyKit config (no KV binding β see data-persistence.md)
βββ package.json β root: partykit + partysocket
βββ docker-compose.yml β Mode B: full-stack with PartyKit in Docker
β οΈ After any backend change (files under
back/), runnpx partykit deploy(from repo root) orcd back && npm run deployto push to Cloudflare. Theproxy-workerdoes not need redeployment for backend-only changes.
Runtime communicationβ
Browser (Host) Browser (Participant)
β β
β PartySocket ws β PartySocket ws
βΌ βΌ
ββββββββββββββββββββββββββββββββββββββββββ
β PartyKit Durable Object β
β room = 4-digit session code β
β β
β this.room.storage β game state β
β this.room.broadcast β fan-out msgs β
ββββββββββββββββββββββββββββββββββββββββββ
Session history: the DO does not write to Cloudflare KV β the
HOSTS_KVbinding has been removed (CF free-plan incompatibility). Session history islocalStorage-only. See Data Persistence.
Durable Object lifecycleβ
| Hook | Called when | What we do |
|---|---|---|
onStart() | DO wakes from eviction | Restore SavedState from storage |
onConnect(ws) | Client opens WebSocket | Register connection (host or participant) |
onMessage(ws, msg) | Message arrives | Parse { type }, dispatch to handler |
alarm() | Timer alarm fires | Auto-reveal current question |
onClose(ws) | Client disconnects | Mark participant offline; start 1-hour host disconnect grace period if host drops |
Host vs participant socket flowβ
Host joins β sends rejoin_host { hostId } on socket open β server validates hostId against stored value β sends host:state_snapshot
Participant joins β sends join_session {} β server creates participant with a unique ADJECTIVE-NOUN pseudonym, broadcasts lobby:updated (the session code is the WebSocket room ID, not a message field)
Participant rejoins β localStorage { id, code } detected on mount β sends rejoin_session { pseudonym } β server sends participant:state_snapshot
Static Frontend, Dynamic Experienceβ
Astro generates a fully static site β plain HTML, CSS, and JS served from a CDN. Yet Sommelier Arena delivers real-time multiplayer gameplay, live leaderboards, and instant answer reveals. There is no application server behind the pages. Three runtime protocols bridge the gap:
WebSocket (PartyKit) β real-time game stateβ
The PartyKit Durable Object (back/game.ts) holds all session state in memory and on Durable Object storage. Host and participant browsers open a PartySocket connection to the room (keyed by the 4-digit session code). Every game event β question broadcast, answer submission, timer expiry, leaderboard update β flows over this single WebSocket. No REST endpoints are needed for gameplay.
React Islands (Zustand) β client-side interactivityβ
Astro pages (/, /host, /play, /admin) are static shells with zero JavaScript by default. Interactive components mount with client:only="react" and manage all UI state through two Zustand stores (hostStore, participantStore). These stores mirror the backend phase machine (waiting β question_open β β¦ β ended), so the frontend is always a projection of server state received over the WebSocket.
HTTP REST (Wine Answers Worker) β reference dataβ
The wine-answers-worker/ Cloudflare Worker serves curated answer collections via simple GET/POST/DELETE endpoints. This data is read-heavy, rarely written, and not time-sensitive β a classic fit for REST over HTTP rather than WebSocket.
Why this worksβ
Static hosting means zero server cost for serving pages and instant page loads from a CDN. The "server" lives at the edge β Cloudflare Durable Objects for real-time state, Workers for reference data. The static shell loads first, then runtime connections bring it to life.
ββββββββββββββββββββββββββββββββββββββββ
β Static Astro Shell β
β (HTML/CSS/JS β served from CDN) β
ββββββββββββββββββββββββββββββββββββββββ€
β React Islands + Zustand Stores β
β (hydrated client-side) β
ββββββββββββ¬βββββββββββ¬βββββββββββββββββ€
β WebSocketβ REST β localStorage β
β PartyKit β Worker β (rejoin data) β
β (game) β (data) β β
ββββββββββββ΄βββββββββββ΄βββββββββββββββββ
Docker vs PartyKit devβ
| Mode A (partykit dev) | Mode B (docker) | |
|---|---|---|
| Backend | npx partykit dev on :1999 | partykit Docker container on :1999 |
| Frontend | npm run dev on :4321 | nginx on :4321 |
| Docs | cd docs-site && npm start | Docker container on :3002 |
| Wine Answers | Not running (or npx wrangler dev on :1998) | wine-answers Docker container on :1998 |
All services run on Cloudflare's infrastructure: zero VMs, zero servers to maintain. The frontend is a static CDN deployment; game state lives in a Durable Object; answer suggestions come from a KV-backed Worker. See Deployment Guide for step-by-step deploy commands.