Skip to main content
Version: 2.0 PartyKit

Configuration & Environments

This page is the single reference for how Sommelier Arena is configured across the three environments you will encounter:

Mode A β€” Local devMode B β€” DockerProduction
FrontendAstro dev server http://localhost:4321nginx container http://localhost:4321Cloudflare Pages (CDN)
PartyKit backendnpx partykit dev ws://localhost:1999PartyKit container (internal, nginx proxied)Cloudflare Workers + Durable Objects
DocumentationDocusaurus dev http://localhost:3002Docusaurus container http://localhost:3002Cloudflare Pages (separate project)
DO storage (room.storage)⚠️ In-memory⚠️ In-memoryβœ… SQLite per DO
Cloudflare KV (HOSTS_KV)❌ Not available❌ Not availableβœ… Real KV namespace
Browser localStorageβœ… Worksβœ… Worksβœ… Works

Environment variables​

Frontend (front/)​

Copy front/.env.example β†’ front/.env.local for local development:

cd front && cp .env.example .env.local
VariableMode A (local)Mode B (Docker)Production
PUBLIC_PARTYKIT_HOSTlocalhost:1999 (direct to partykit dev)localhost:4321 (baked at Docker build time; nginx proxies /parties/* to back:1999)sommelier-arena.<username>.partykit.dev
PUBLIC_DOCS_URL(optional) http://localhost:3002/docs(optional) http://localhost:3002/docshttps://your-domain/docs

Docker note: In Mode B, PUBLIC_PARTYKIT_HOST is a Docker build arg baked into the Astro static build β€” do not rely on .env.local for Docker builds. Pass it via docker-compose.yml or docker build --build-arg.

Backend (back/)​

The backend is a PartyKit Durable Object (back/game.ts). It has no .env file. Configuration comes from:

  • partykit.json β€” KV namespace bindings, DO class names
  • wrangler secret put or CI secrets β€” production secrets

Docs site (docs-site/)​

Copy docs-site/.env.example β†’ docs-site/.env:

cd docs-site && cp .env.example .env
VariableLocalDockerProduction
DOCS_BASE_URL/ (root)/docs (baked at build time)/docs (Cloudflare Pages base path)

Master config comparison​

All layers in one view:

LayerSettingMode A (local)Mode B (Docker)Production
FrontendPUBLIC_PARTYKIT_HOSTfront/.env.local β†’ localhost:1999Docker build arg β†’ localhost:4321Cloudflare Pages env β†’ <project>.partykit.dev
FrontendServingAstro dev server :4321nginx container (mapped 4321:3000)Cloudflare Pages CDN
BackendPartyKitnpx partykit dev --port 1999PartyKit container (internal :1999)Cloudflare Workers (Durable Objects)
BackendDO storageIn-memory (resets on restart)In-memorySQLite (persistent across DO evictions)
BackendHOSTS_KVNot availableNot availableCloudflare KV namespace (bound in partykit.json)
DocsServingDocusaurus dev :3002nginx container (mapped 3002:80)Cloudflare Pages (/docs)
DocsDOCS_BASE_URL//docs/docs
Proxy WorkerDOCS_ORIGINN/AN/AInjected via Wrangler --var or Worker env
DeploymentCommandnpx partykit devdocker-compose up --buildgit push (Pages) + npx partykit deploy + npx wrangler deploy

nginx.conf explained​

The Docker front container uses nginx as a static file server. Here's what front/nginx.conf does and why:

What it does​

# 1. Listen on container port 3000 (Docker maps host port 4321 β†’ container 3000)
listen 3000;

# 2. Use relative Location headers so port mapping doesn't strip the host port
absolute_redirect off;

# 3. Proxy WebSocket + HTTP to the PartyKit backend container
location /parties/ {
proxy_pass http://back:1999/parties/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}

# 4. SPA routing: serve index.html for any unknown path (/host, /play, ...)
location / {
try_files $uri $uri/index.html /index.html;
}

Why nginx:

  • Astro builds to pure static files (output: static in astro.config.mjs) β€” no Node.js server runs at request time. nginx serves these HTML/JS/CSS files.
  • The browser must access static files and WebSocket on the same origin to avoid CORS issues. nginx handles both in one container.
  • absolute_redirect off prevents port-stripping bugs: without it, nginx's 301 redirects use the internal port (3000), causing browsers to cache broken redirects to http://localhost/host instead of http://localhost:4321/host.
  • try_files $uri $uri/index.html /index.html is the standard SPA pattern: nginx checks for the exact file, then path/index.html, then falls back to index.html β€” no redirect triggered for /host or /play.

Why we need it for E2E tests​

The Playwright E2E tests run against the Docker stack (Mode B). Without nginx:

  1. There's no server to serve the Astro static build
  2. WebSocket connections from the browser to /parties/* have no route to the back container

Alternatives to nginx​

AlternativeTrade-offs
CaddySimpler config syntax, auto-HTTPS; same capabilities. Swap nginx:alpine for caddy:alpine and replace nginx.conf with a Caddyfile.
Mode A (no Docker)npm run dev in front/ uses Astro's built-in dev server on :4321. SPA routing and WebSocket proxy work natively. No nginx needed. This is the recommended approach for daily development.
Apache httpdHeavier; works but nginx:alpine is smaller.
Node.js + http-proxy-middlewareMore complex Docker setup; not production-like.

For local development (Mode A), you never need nginx. nginx is only required for the production-like Docker build used in E2E tests (Mode B).


Storage layers​

1. Browser localStorage β€” always available​

KeyContentsUsed by
sommelier-arena-host-{hostId}SessionListEntry[] β€” host's session historyHost Dashboard
sommelier-arena-rejoin{ rejoinToken, code, pseudonym } β€” participant credentialsParticipant rejoin

Works identically in all environments. Managed by front/src/lib/sessionStorage.ts.

⚠️ Local dev note: Because KV is not available locally, the Host Dashboard session list comes only from localStorage. Use the πŸ—‘ button to clean up stale sessions.

2. Durable Object storage (room.storage) β€” in-memory locally​

KeyContents
'state'Full SavedState snapshot (restored after DO eviction)
'hostId'Host re-authentication token
'participant:{rejoinToken}'Participant rejoin credentials
'response:{participantId}:{questionId}'Answer record for accurate scoring
  • Local / Docker: In-memory. Restarting partykit dev or docker-compose down clears all sessions.
  • Production: SQLite-backed. onStart() in back/game.ts reads saved state when the DO wakes from cold storage.

3. Cloudflare KV (HOSTS_KV) β€” production only​

Cross-session host index. Key: host:{hostId} β†’ SessionListEntry[].

Not available in Mode A or Mode B. The backend wraps all KV writes in try/catch and silently skips them locally β€” the app continues working via localStorage.


How a static site runs multiplayer​

Astro builds to HTML + JS + CSS. No server runs at request time. Yet the app is live-multiplayer. How?

  1. Astro static build β†’ pure files deployed to Cloudflare Pages CDN globally.
  2. PartySocket bundled in JS β†’ compiled into the JS bundle at build time.
  3. Browser opens WebSocket to ws://your-domain.com/parties/main/{sessionCode} (or ws://localhost:4321/parties/main/{code} in Mode B via nginx proxy).
  4. All game state flows over WebSocket β€” no REST API. Events: host:start, participant:submit_answer, game:question, game:timer_tick, game:answer_revealed, etc.
  5. React islands handle reactivity β€” Zustand stores (hostStore, participantStore) hold client-side state, updated by socket hooks.
Browser                           Cloudflare (production)
β”‚ β”‚
β”‚ GET https://your-domain.com/host β”‚
│──────────────────────────────────────>β”‚ Pages CDN β†’ static HTML + JS
β”‚<──────────────────────────────────────│
β”‚ β”‚
β”‚ WS wss://your-domain.com/parties/main/1234
│──────────────────────────────────────>β”‚ Workers β†’ Durable Object (game.ts)
β”‚<══════════════════════════════════════│ real-time game events (JSON)

Testing & Preview (docs site)​

Docusaurus local preview matching production​

Production serves docs at /docs (via the Proxy Worker). To preview with the same path locally:

cd docs-site
npm run build:local # builds with DOCS_BASE_URL=/docs
npm run serve:docs # serves at http://localhost:3002/docs

Do not open build/index.html directly with file:// when the build used /docs β€” asset paths are absolute and the browser won't find them.

Preview at root (simpler)​

cd docs-site
npm run build # builds with DOCS_BASE_URL=/
npx docusaurus serve # serves at http://localhost:3000/

Troubleshooting​

SymptomCauseFix
http://localhost:3002/docs refuses connectionServe script not runningRun npm run serve:docs
"Your site did not load properly" bannerOpened build/index.html with file://Use npm run serve:docs instead
Session history missing after restartExpected in dev β€” DO storage is ephemeralUse localStorage; no fix needed
Participant can't rejoin after partykit dev restartRejoin tokens in DO storage (in-memory)Expected; restart the session
Session list only shows in one browserKV not available locallyExpected; localStorage is per-browser