Files
hermes-workspace/playground-ws-worker
Eric 4f177f9b8d feat(tasks): unify Workspace task board with Hermes Kanban backend (#311) (#348)
* wip(hermesworld): viral sprint checkpoint - landing rebuild + character pipeline scaffold

- standalone /hermes-world and /world routes bypass workspace shell
- root overlay leaks gated for landing + game surfaces
- character pipeline scaffolding (player/npc/glb-body components)
- canonical asset path public/assets/hermesworld/characters/
- docs: landing-page-spec, graphics-usability-plan, agora-believable-checklist, master-roadmap
- handoff at memory/goals/2026-05-05-hermesworld-viral-sprint/handoff.md

Local-only checkpoint. Not for upstream yet.

* feat(playground): persistent admin mode toggle with shield button

- Admin mode now persists via localStorage (key: hermes-playground-admin)
- Shield icon button in HUD (right rail, below focus toggle, md+)
- Click toggles admin panel and saves preference
- ?admin=1 URL param still works as override
- gitignore swarm worker scratch dirs

Mission: memory/swarm/missions/2026-05-05-pr-triage.md (5 swarm lanes dispatched on 19 open PRs, no-merge contract)

* feat(landing): add Play Now CTAs to HermesWorld landing

- Hero: Play Now (primary, gold), View on GitHub (demoted), Read Roadmap
- Header nav: Play badge (highlighted gold)
- Final CTA: Play Now (primary), GitHub + Roadmap (secondary)

All Play buttons go to /playground which mounts the title screen
(username + character customizer + Enter). Sets up the public-URL
deploy: hermes-world.ai → / serves landing → click Play → /playground.

* fix(tasks): use shared kanban backend

---------

Co-authored-by: Aurora release bot <release@outsourc-e.com>
2026-05-05 16:46:24 -04:00
..

Hermes Playground multiplayer hub (Cloudflare Worker)

Drop-in port of scripts/playground-ws.mjs to Cloudflare Workers + Durable Objects. Free tier covers a hackathon and well beyond. Zero cold starts. Edge-deployed.

Why CF + DO over Fly.io / Render / Railway

  • Free tier: 100k req/day on Workers, 1M+ DO req/mo. WebSocket connections count per-message, not per-connection — a presence broadcast every 200 ms across 20 players is well under the limit.
  • Zero cold starts (vs Fly.io free tier which idles VMs after inactivity, and Render free which has a 30-50s cold start that kills demos).
  • One Durable Object instance is the canonical "lobby/room" pattern — strong consistency, no Redis needed.
  • Globally edge-deployed: a player in Tokyo and a player in NYC both connect to the closest edge, then route to the single DO holding game state.

Files

  • src/worker.ts — entry + PlaygroundHub Durable Object class
  • wrangler.toml — DO binding + migration
  • package.json / tsconfig.json — build deps

Endpoints

  • GET /playground — WebSocket upgrade (presence + chat fan-out, mirrors the Node sidecar protocol)
  • GET /stats — JSON { online, byWorld, peakToday, peakDay, ts } for the HUD badge
  • GET /health — JSON { ok: true, online, ts }

Deploy (~10 min, requires Eric's CF account)

cd playground-ws-worker
pnpm install                 # installs wrangler
pnpm wrangler login          # one-time browser auth
pnpm deploy                  # publishes to <name>.<your-subdomain>.workers.dev

Then in workspace .env.production:

VITE_PLAYGROUND_WS_URL=wss://hermes-playground-ws.<your-subdomain>.workers.dev/playground
VITE_PLAYGROUND_STATS_URL=https://hermes-playground-ws.<your-subdomain>.workers.dev/stats

(Custom domain optional via Workers Routes — wss://hub.hermes-playground.app.)

Local dev

pnpm wrangler dev            # hot-reload on http://localhost:8787
# In hermes-workspace:
VITE_PLAYGROUND_WS_URL=ws://localhost:8787/playground pnpm dev

Protocol parity

Wire format is identical to scripts/playground-ws.mjs. The client (src/screens/playground/hooks/use-playground-multiplayer.ts) connects unchanged.

Messages:

  • { kind: 'presence', id, x, y, z, yaw, name, color, worldId, avatar?, ... }
  • { kind: 'chat', id, text, ts }
  • { kind: 'leave', id }
  • Server-emitted: { kind: 'hello', server, ts }

State model

  • presence: Map<id, lastWire> — fan-out + bootstrap on connect
  • chatRing: array<chat> — last 50 messages, replayed to newcomers
  • peakToday — persisted in DO storage for stats endpoint

Stale presence is pruned every 1 s via DO alarms (cheaper than setInterval, and survives instance hibernation).

Cost ceiling

For 100 concurrent players sending presence at 5 Hz:

  • 100 × 5 × 60 × 60 × 24 = 43.2M msgs/day → still inside free tier for outbound (Workers count requests, not WS messages). If usage explodes:
    • Workers Paid: $5/mo for 10M req/day baseline.
    • DO storage: trivial (presence in memory, peak in storage).

Hardening (post-hackathon)

  • Add rate limiting per playerId (token bucket in DO state).
  • Multi-room: route by ?room= to idFromName(roomId).
  • Anti-spoof: require signed JWT for presence.id.
  • Replace presence ring with state.storage.transaction for crash recovery.