Skip to main content
Version: 2.0 PartyKit

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​

KeyTypeValue
'state'SavedStateFull session snapshot (phase, wines, questions, currentIndices, timerMs)
'hostId'stringTANNIC-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_KV binding is currently disabled. The bindings.kv.HOSTS_KV entry has been removed from partykit.json. Session history is stored in localStorage only.

Why it was removed: When CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_TOKEN are set, PartyKit deploys the entire Worker (including Durable Objects) to your own Cloudflare account. On the free Cloudflare plan, new DO namespaces require a new_sqlite_classes migration. 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 in back/persistence.ts is wrapped in try/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)​

  1. Add the binding back to partykit.json:
    {
    "bindings": {
    "kv": { "HOSTS_KV": "98082bb612964007aac177820469dddc" }
    }
    }
  2. 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.
  3. 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.toml must be present in the current directory for --binding or --local to 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​

EventDO storageHOSTS_KVlocalStorage
DO eviction (idle)βœ… restored on next connectionN/A (disabled)βœ… unchanged
Page refresh (host)βœ… rejoin_host restores full stateN/A (disabled)βœ… unchanged
Page refresh (participant)βœ… rejoin_session { pseudonym } restores stateβ€”βœ… unchanged
Server restart (local dev)❌ in-memory lost; storage persistsN/Aβœ… unchanged
partykit dev restart❌ local storage clearedβ€”βœ… unchanged

Note: In npx partykit dev mode, DO storage is in-memory only. Production Cloudflare Workers use real SQLite-backed DO storage.

localStorage (browser)​

KeyValueCleared when
sommelierArena:hostIdTANNIC-FALCONNever (user clears browser data)
sommelierArena:rejoin{ id, code } β€” participant's pseudonym and session codesession: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.