Data Persistence
Durable Object storageβ
The GameSession Durable Object persists all state to built-in SQLite-backed storage. State survives DO eviction (Cloudflare may evict idle DOs after ~30 s of inactivity but restores from storage on next request).
DO storage keysβ
| Key | Type | Value |
|---|---|---|
'state' | SavedState | Full session snapshot (phase, wines, questions, currentIndices, timerMs) |
'hostId' | string | TANNIC-FALCON β used to authenticate rejoin_host |
'participant:{pseudonym}' | object | { id, pseudonym, score, connected, answeredQuestions: string[] } β keyed by the participant's ADJECTIVE-NOUN pseudonym |
'response:{participantId}:{questionId}' | object | { optionId, correct, points } |
What SavedState containsβ
interface SavedState {
phase: SessionPhase;
title: string;
wines: Wine[]; // full wine + question data
currentRoundIndex: number;
currentQuestionIndex: number;
timerSeconds: number; // configured at creation
remainingMs: number; // live timer state
createdAt: string;
}
Cloudflare KV β HOSTS_KV (disabled)β
β οΈ The
HOSTS_KVbinding is currently disabled. Thebindings.kv.HOSTS_KVentry has been removed frompartykit.json. Session history is stored inlocalStorageonly.Why it was removed: When
CLOUDFLARE_ACCOUNT_ID+CLOUDFLARE_API_TOKENare set, PartyKit deploys the entire Worker (including Durable Objects) to your own Cloudflare account. On the free Cloudflare plan, new DO namespaces require anew_sqlite_classesmigration. PartyKit does not support this migration config, so the deploy fails. Removing the binding allows PartyKit to deploy to its own infrastructure with no plan restrictions.The
upsertKvSession()function inback/persistence.tsis wrapped intry/catchβ it silently skips KV writes when no binding is present, so the game runs correctly without it.
KV was used for the host sessions index β a list of all sessions created by a given hostId.
KV namespace (for reference)β
Name: SOMMELIER_HOSTS
Binding key (when enabled): HOSTS_KV in partykit.json
KV key / value format (for reference)β
host:TANNIC-FALCON
[
{
"code": "4829",
"title": "Wine Night 1",
"createdAt": "2025-01-20T19:00:00.000Z",
"status": "ended",
"participantCount": 8,
"finalRankings": [
{ "pseudonym": "Alice", "score": 500, "rank": 1 }
]
}
]
Re-enabling HOSTS_KV (requires a paid Cloudflare account)β
- Add the binding back to
partykit.json:{
"bindings": {
"kv": { "HOSTS_KV": "98082bb612964007aac177820469dddc" }
}
} - Create a Cloudflare API token with "Edit Cloudflare Workers" template (Workers Scripts:Edit
- Workers KV Storage:Edit + Account Settings:Read) at
dash.cloudflare.com/profile/api-tokens.
- Workers KV Storage:Edit + Account Settings:Read) at
- Deploy:
CLOUDFLARE_ACCOUNT_ID=378a18b7a23a0fc5fda12864848b7f09 \
CLOUDFLARE_API_TOKEN=<token> \
npx partykit deploy
Inspecting KV data (production)β
The sections below apply to WINE_ANSWERS_KV only. HOSTS_KV is currently disabled.
β οΈ Local dev vs production β critical distinctionβ
wrangler dev --local stores KV data in .wrangler/state/ on your machine.
It does not write to the real Cloudflare KV namespaces.
This means:
- Anything you seed via
npm run dev(local worker) is only visible locally. wrangler kv key list --namespace-id=...always queries production KV.- If production KV appears empty, you need to seed it explicitly β see Seeding production below.
Inspect local dev KV (data from npm run dev)β
Must run from
wine-answers-worker/βwrangler.tomlmust be present in the current directory for--bindingor--localto work. Running from the repo root will fail with "No KV Namespaces configured".
cd wine-answers-worker
# List all category keys stored locally
npx wrangler kv key list --binding=WINE_ANSWERS_KV --local
# Get a specific category's answers locally
npx wrangler kv key get "color" --binding=WINE_ANSWERS_KV --local
npx wrangler kv key get "grape_variety" --binding=WINE_ANSWERS_KV --local
HOSTS_KV (SOMMELIER_HOSTS) is managed by PartyKit and is currently disabled β see Cloudflare KV β HOSTS_KV (disabled) above.
Inspect production KVβ
wrangler kv key list --namespace-id=... always queries production. Requires npx wrangler login.
SOMMELIER_HOSTS β host session index (disabled)β
Namespace ID: 98082bb612964007aac177820469dddc
The namespace exists in the Cloudflare account but the binding is disabled. These commands will return empty results until the binding is re-enabled (paid Cloudflare plan required).
# List all keys (one per hostId that has created a session)
npx wrangler kv key list --namespace-id=98082bb612964007aac177820469dddc
# Get sessions for a specific host
npx wrangler kv key get "host:TANNIC-FALCON" \
--namespace-id=98082bb612964007aac177820469dddc
# Delete a host's session history (cleanup)
npx wrangler kv key delete "host:TANNIC-FALCON" \
--namespace-id=98082bb612964007aac177820469dddc
WINE_ANSWERS_KV β curated answer listsβ
Namespace ID: c6e83a314d254ca5801f0fa90a19c746
# List all category keys
npx wrangler kv key list --namespace-id=c6e83a314d254ca5801f0fa90a19c746
# Get answers for a specific category
npx wrangler kv key get "color" --namespace-id=c6e83a314d254ca5801f0fa90a19c746
npx wrangler kv key get "grape_variety" --namespace-id=c6e83a314d254ca5801f0fa90a19c746
# Overwrite a category's answer list manually (JSON array of strings)
npx wrangler kv key put "region" '["Bordeaux","Bourgogne","Alsace","Loire"]' \
--namespace-id=c6e83a314d254ca5801f0fa90a19c746
Seeding productionβ
The seed script sends data through the Worker HTTP API (not directly to KV), so the ADMIN_SECRET Cloudflare secret must be set first:
# One-time setup: store the secret in Cloudflare (never put it in wrangler.toml)
cd wine-answers-worker
npx wrangler secret put ADMIN_SECRET
# Then seed production (from wine-answers-worker/)
WINE_ANSWERS_URL=https://sommelier-arena-wine-answers.<your-subdomain>.workers.dev \
ADMIN_SECRET=<your-secret> \
npm run seed:prod
Or seed locally (writes to .wrangler/state/, visible only in local dev):
cd wine-answers-worker
npm run seed # seeds http://localhost:1998 with secret "changeme"
What survivesβ
| Event | DO storage | HOSTS_KV | localStorage |
|---|---|---|---|
| DO eviction (idle) | β restored on next connection | N/A (disabled) | β unchanged |
| Page refresh (host) | β
rejoin_host restores full state | N/A (disabled) | β unchanged |
| Page refresh (participant) | β
rejoin_session { pseudonym } restores state | β | β unchanged |
| Server restart (local dev) | β in-memory lost; storage persists | N/A | β unchanged |
partykit dev restart | β local storage cleared | β | β unchanged |
Note: In
npx partykit devmode, DO storage is in-memory only. Production Cloudflare Workers use real SQLite-backed DO storage.
localStorage (browser)β
| Key | Value | Cleared when |
|---|---|---|
sommelierArena:hostId | TANNIC-FALCON | Never (user clears browser data) |
sommelierArena:rejoin | { id, code } β participant's pseudonym and session code | session:ended received |
Cloudflare KV (WINE_ANSWERS_KV)β
The Wine Answers Worker uses a separate KV namespace for curated answer data.
KV namespaceβ
Name: WINE_ANSWERS_KV
Binding in wine-answers-worker/wrangler.toml: WINE_ANSWERS_KV
KV key formatβ
color
region
grape_variety
vintage_year
wine_name
Each key is a question category name.
KV value formatβ
["Bordeaux", "Burgundy", "Champagne", "Napa Valley", "Rioja"]
A JSON array of curated answer strings for that category.
In-Memory Data Modelβ
The following shows the runtime data shapes held in memory by the GameSession Durable Object (one instance per session code):
Game Session (one per room.id / session code):
βββ wines: Wine[] β list of wines with questions and options
βββ participants: Map β keyed by pseudonym (ADJECTIVE-NOUN)
β βββ id, socketId, pseudonym
β βββ score, connected
β βββ answeredQuestions: Set
βββ phase: SessionPhase β waiting | question_open | question_paused | question_revealed | question_leaderboard | round_leaderboard | ended
βββ currentRound β index into wines[]
βββ currentQuestion β index into current wine's questions[]
βββ timerSeconds β configured timer duration (15β120s)
βββ timerRemainingMs β live countdown
βββ hostId β e.g. "TANNIC-FALCON"
βββ sessionTitle β e.g. "Friday Wine Night"
Each Wine contains 5 questions (one per fixed category: color, region, grape_variety, vintage_year, wine_name). Each question carries 4 answer options (1 correct, 3 distractors). Clients only learn which option is correct when game:answer_revealed is emitted β options are sent without the correct flag during active play.