Skip to main content
Version: 0.2.0 β€” PartyKit

Architecture

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
β”œβ”€β”€ partykit.json ← PartyKit config + KV binding
β”œβ”€β”€ package.json ← root: partykit + partysocket
└── docker-compose.yml ← Mode B: full-stack with PartyKit in Docker

⚠️ After any backend change (files under back/), run npx partykit deploy (from repo root) or cd back && npm run deploy to push to Cloudflare. The proxy-worker does 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 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ KV write
β–Ό
Cloudflare KV
SOMMELIER_HOSTS
key: host:{TANNIC-FALCON}

Durable Object lifecycle​

HookCalled whenWhat we do
onStart()DO wakes from evictionRestore SavedState from storage
onConnect(ws)Client opens WebSocketRegister connection (host or participant)
onMessage(ws, msg)Message arrivesParse { type }, dispatch to handler
alarm()Timer alarm firesAuto-reveal current question
onClose(ws)Client disconnectsMark participant offline; detect host drop

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

Docker vs PartyKit dev​

Mode A (partykit dev)Mode B (docker)
Backendnpx partykit dev on :1999partykit Docker container on :1999
Frontendnpm run dev on :4321nginx on :4321
Docscd docs-site && npm startDocker container on :3002