Skip to main content

Live Demo — Phase 1 Plan

The detailed plan for the simplified model.

note

No agent on the Unreal Engine side. Our backend is a reservation service (queue + 15-minute timer + token); the frontend embeds the ready-made Pixel Streaming player in an iframe and tears the connection down itself at the 15-minute mark. DevOps provides the nginx token gate and maxPlayerCount=1.

Stack: backend Express + JS (ESM), with ws and wsServer.js already in place; Prisma/Postgres; state in memory (Redis later). Frontend React/TS.

Phase 1 decisions

  • State is in-memory (single-process Express). Redis is the scaling path.
  • The 15-minute window starts at assigned (token TTL = 15 minutes). We do not wait for "peer connected" — the player lives in our page and we do not track the peer (cross-origin iframe). This removes the entire agent protocol.
  • One WebSocket connection per user; one active reservation per user.

Contract A — client ↔ orchestrator (/ws/demo)

Auth: the existing httpOnly session (cookie in the WebSocket handshake) plus an Origin check. Messages are JSON { type, ... }.

Client → server

typePayload
join{ avatar? } — a specific domain when avatar choice is enabled, otherwise any.
leave
heartbeat— keepalive; if it stops, the slot is freed early.

Server → client

typePayloadMeaning
queued{ position, etaSec }In the queue.
position{ position, etaSec }Position update.
assigned{ sessionId, domain, url, token, endsAt }A domain is free — embed the iframe; the timer is already running.
ending{ inSec }60-second warning.
ended{ reason, cooldownUntil }Session finished.
error{ code }One of not_authed, already_queued, in_cooldown, queue_full.

reason is one of timeout, left, disconnect. url is https://{domain}/?token=...&HoveringMouse=true&FakeMouseWithTouches=true.

Contract B — token gate (nginx → our backend)

  • On assigned, the backend mints a token valid until endsAt and bound to {userId, domain, sessionId}.
  • The frontend embeds the iframe with ?token=....
  • nginx on each domain runs auth_requestGET /internal/demo/validate?token=...&domain=...200 (valid, not expired, domain matches) or 403. This blocks direct access and reconnection after the limit.
  • Alternative: a signed JWT that nginx validates locally, if DevOps prefers that.

Contract C — frontend lifecycle

On assigned, mount <iframe src=url> with a countdown overlay running to endsAt. At endsAt, on leave, or on close, unmount the iframe — which tears down WebRTC and frees the slot — and send leave. Send a heartbeat over the WebSocket while the session is active.

Data (in-memory)

  • domains: a Map of { avatar, url, status: free|busy, reservedFor, sessionId, endsAt } — 3 entries from config.
  • queue: a per-avatar Map or one shared array, per the product decision; plus ticketsByUser (one per user).
  • sessions: a Map of { userId, domain, startedAt, endsAt, token }.
  • tokens: a Map from token to { userId, domain, sessionId, expiresAt }.
  • cooldownByUser and clientsByUser (WebSockets). A single setInterval(1000) drives the 60-second warning, the 15-minute end, and heartbeat timeouts.

Postgres DemoSession: id, userId, domain, queuedAt, startedAt, endedAt, endReason, durationSec.

Assignment loop

Triggers: join, a domain becoming free (leave, timeout, or heartbeat timeout), and each tick.

One step: given a free domain and a non-empty queue, take the next ticket, mark the domain busy, create the session and token, and send assigned with endsAt = now + 15 min. A cooldown applies after the session ends.

Files

Backend (JS ESM)

FileChange
backend/prisma/schema.prismaAdd DemoSession (+ migrate).
backend/src/config/config.jsAdd demo: domains[], sessionMs, warnMs, cooldownMs, heartbeatTimeoutMs, WS path.
backend/src/services/demo/reservation.jsState, queue, assignment, lifecycle.
backend/src/services/demo/tokens.jsMint / validate.
backend/src/services/demo/timers.jsThe one-second worker.
backend/src/services/demo/clientHub.jsClient WebSockets per userId, plus events.
backend/src/ws/demoClient.jsMessage handler.
backend/src/wsServer.jsExtend with /ws/demo (cookie auth).
backend/src/controllers/demoController.js, routes/demoRoutes.jsGET /api/demo/stats (admin) and GET /internal/demo/validate (internal network only).
backend/src/middlewares/Reuse auth for the WebSocket; add an internal-only guard for /internal.

A mock agent is not needed — in development the iframe points at a stub or at the real domain.

Frontend (React/TS)

FilePurpose
frontend/src/Demo/live/useDemoSession.tsWebSocket client and the idle→queued→assigned→active→ended/error state machine.
frontend/src/Demo/live/DemoQueue.tsxQueue UI — position, ETA, cancel.
frontend/src/Demo/live/DemoPlayer.tsxIframe wrapper plus the countdown overlay.
frontend/src/Demo/live/DemoCountdown.tsxThe 15-minute countdown and 60-second warning.
frontend/src/Demo/live/LiveDemoPage.tsxThe page and its login gate.
frontend/src/App/App.tsxProtected <Route path="demo">.
frontend/src/sections/Demo/...Live Demo button → /demo or /signin?next=/demo.

Sequence

  1. Prisma DemoSession and config.demo (3 domains).
  2. wsServer.js/ws/demo with auth and the client handler; join → queue → queued/position.
  3. Assignment, token, and assigned; the one-second timer driving ending/ended; leave and heartbeat freeing the domain.
  4. /internal/demo/validate (for nginx) and /api/demo/stats.
  5. Frontend: the hook and state machine, the queue, the iframe player and countdown, the route, and the button gate.
  6. End-to-end with a test URL (no real Pixel Streaming), then the real domains once DevOps is ready.
  7. Tail work: Postgres persistence, cooldown, Prometheus /metrics.

To confirm with DevOps

  1. An nginx auth_request token gate on each domain — possible?
  2. Signalling maxPlayerCount=1 (or an equivalent) — supported?
  3. Three HTTPS domains (demo-adam, demo-anna, demo-<third>.amadeq.com) — agreed?