Live Demo — Phase 1 Plan
The detailed plan for the simplified model.
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
type | Payload |
|---|---|
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
type | Payload | Meaning |
|---|---|---|
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 untilendsAtand bound to{userId, domain, sessionId}. - The frontend embeds the iframe with
?token=.... - nginx on each domain runs
auth_request→GET /internal/demo/validate?token=...&domain=...→200(valid, not expired, domain matches) or403. 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: aMapof{ avatar, url, status: free|busy, reservedFor, sessionId, endsAt }— 3 entries from config.queue: a per-avatarMapor one shared array, per the product decision; plusticketsByUser(one per user).sessions: aMapof{ userId, domain, startedAt, endsAt, token }.tokens: aMapfrom token to{ userId, domain, sessionId, expiresAt }.cooldownByUserandclientsByUser(WebSockets). A singlesetInterval(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)
| File | Change |
|---|---|
backend/prisma/schema.prisma | Add DemoSession (+ migrate). |
backend/src/config/config.js | Add demo: domains[], sessionMs, warnMs, cooldownMs, heartbeatTimeoutMs, WS path. |
backend/src/services/demo/reservation.js | State, queue, assignment, lifecycle. |
backend/src/services/demo/tokens.js | Mint / validate. |
backend/src/services/demo/timers.js | The one-second worker. |
backend/src/services/demo/clientHub.js | Client WebSockets per userId, plus events. |
backend/src/ws/demoClient.js | Message handler. |
backend/src/wsServer.js | Extend with /ws/demo (cookie auth). |
backend/src/controllers/demoController.js, routes/demoRoutes.js | GET /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)
| File | Purpose |
|---|---|
frontend/src/Demo/live/useDemoSession.ts | WebSocket client and the idle→queued→assigned→active→ended/error state machine. |
frontend/src/Demo/live/DemoQueue.tsx | Queue UI — position, ETA, cancel. |
frontend/src/Demo/live/DemoPlayer.tsx | Iframe wrapper plus the countdown overlay. |
frontend/src/Demo/live/DemoCountdown.tsx | The 15-minute countdown and 60-second warning. |
frontend/src/Demo/live/LiveDemoPage.tsx | The page and its login gate. |
frontend/src/App/App.tsx | Protected <Route path="demo">. |
frontend/src/sections/Demo/... | Live Demo button → /demo or /signin?next=/demo. |
Sequence
- Prisma
DemoSessionandconfig.demo(3 domains). wsServer.js→/ws/demowith auth and the client handler;join→ queue →queued/position.- Assignment, token, and
assigned; the one-second timer drivingending/ended;leaveand heartbeat freeing the domain. /internal/demo/validate(for nginx) and/api/demo/stats.- Frontend: the hook and state machine, the queue, the iframe player and countdown, the route, and the button gate.
- End-to-end with a test URL (no real Pixel Streaming), then the real domains once DevOps is ready.
- Tail work: Postgres persistence, cooldown, Prometheus
/metrics.
To confirm with DevOps
- An nginx
auth_requesttoken gate on each domain — possible? - Signalling
maxPlayerCount=1(or an equivalent) — supported? - Three HTTPS domains (
demo-adam,demo-anna,demo-<third>.amadeq.com) — agreed?