Live Demo Architecture
The "try a demo with an avatar for free" feature.
Simplified model (2026-06-22): Pixel Streaming already exists and runs independently — one domain per avatar. All we need is a thin reservation layer (queue + 15-minute limit + gate), with no custom agent on the Unreal Engine side.
Status: design / not yet implemented.
Agreed decisions
| Topic | Choice |
|---|---|
| Access | Through sign-in / sign-up (identity = account). |
| Slots | 3 domains = 3 avatars, each an independent PS stack, one user at a time. |
| Hosting | Own bare-metal Linux + RTX 5090 (~3 GB per instance). |
| 15-minute control | The frontend removes the iframe, and the token expires (no agent). |
| No sharing | maxPlayerCount=1 on signalling (a DevOps config item). |
The Pixel Streaming setup, as reported by the team: each domain is an Epic Signalling Web Server (start.sh ... --streamer_port 8888 --player_port 8080 --sfu_port 8890) plus a UE application (-PixelStreamingURL=ws://127.0.0.1:8888). The player is served by signalling itself on player_port. Domains: demo-adam.amadeq.com, demo-anna.amadeq.com, demo-<third>.amadeq.com, over HTTPS through nginx.
Components
Our backend — reservation/queue service
A module inside the existing Express + ws application.
- Login gate (the existing httpOnly session).
- A registry of the domains (3, from config):
{ id/avatar, url, status: free|busy, reservedFor, endsAt }. - A queue per domain (per avatar), or one shared queue — a product decision.
- Assignment of a free domain, a 15-minute reservation, and issuance of an access token valid for the session and bound to
{userId, domain, sessionId}. - A WebSocket to the frontend (queue, assignment, countdown, heartbeat). It never touches Pixel Streaming.
- An endpoint for nginx:
GET /internal/demo/validate?token&domain→200/403.
Frontend
- The Live Demo button is shown only to signed-in users; otherwise it routes to
/signin?next=/demo. - Opens a WebSocket to the backend and shows the queue screen (position / ETA).
- On
assigned, it embeds the player in an iframe —https://{domain}/?token=...&HoveringMouse=true&FakeMouseWithTouches=true— with a 15-minute countdown overlay. - At the 15-minute mark, on "leave", or on close, it removes the iframe (which tears down WebRTC and frees the slot) and sends
leaveover the WebSocket. - Sends a heartbeat over the WebSocket, so a closed tab frees the slot early.
DevOps / Pixel Streaming side
Configuration only — no Unreal Engine code.
- Each domain sits behind nginx + TLS, with
auth_requestpointing at our/internal/demo/validate(a missing or expired token gives403). This blocks direct access and reconnection after the limit. - Signalling runs with
maxPlayerCount=1, so two users cannot land on the same stream.
How the requirements are met
- No two users on one instance —
maxPlayerCount=1on the PS side, plus we never assign a busy domain. - 15 minutes — our server timer is the source of truth for the reservation, the frontend removes the iframe (tearing down the stream), and the token expires so there is no reconnect. No separate process is needed to kick an established stream, because the player lives inside our page.
- Login, queue, fairness — our backend.
- Direct URL access — the nginx token gate.
Lifecycle
QUEUED ──(domain free)──► ASSIGNED (token + url, endsAt) ──► ACTIVE (iframe streaming, counting down)
│
(15 min / leave / tab closed) │
▼
frontend removes iframe → WS leave → slot free → cooldown
Residual case: if the tab is closed without a leave, the slot is held until the 15-minute timer expires. The heartbeat shortens that window.
Observability and capacity planning
The backend sees every event — queueing, assignment, start/end, waiting — so it is the source of metrics.
- Metrics: domains free/busy and utilisation %; queue length, peak, and waiting time (p50/p95); session starts and ends, duration, share reaching the full 15 minutes, abandons; session throughput per hour.
- Exposure:
GET /admin/demo/stats(live snapshot) → Prometheus/metrics→ Grafana → Postgresdemo_sessionfor reports. - Signals to add capacity: p95 waiting time consistently above 10–15 minutes; rising abandons; ~100 % utilisation at peak with a non-empty queue; arrivals per hour approaching capacity.
- Capacity (Little's Law):
N ≥ λ·T, where λ is arrivals per minute at peak and T is duration (≤ 15 min). For 3 slots that is roughly3 × 60/15 ≈ 12sessions per hour. The pool is fixed, so adding a domain or GPU is a manual decision driven by those signals.
Data
In-memory (single-process Express, so no races): domains, queue(s), sessions, tokens, cooldowns, client WebSockets, and a one-second timer. Redis becomes necessary once there is more than one backend process, or when state must survive a restart.
Postgres demo_session: id, userId, domain, queuedAt, startedAt, endedAt, endReason, durationSec.
Open questions
- DevOps: is an nginx token gate (
auth_request) possible on each domain? - DevOps: is signalling
maxPlayerCount=1(or an equivalent) supported? - DevOps: are the 3 HTTPS domains confirmed?
- Product: does the user choose an avatar (3 independent queues), or take any free one (a shared queue)?
Phases
- Phase 0 (DevOps): 3 HTTPS domains, an nginx token gate, and
maxPlayerCount=1. - Phase 1 (us): the reservation/queue service (in-memory), the login gate, the 15-minute timer, the token, and
/internal/demo/validate; on the frontend, the queue, the iframe player, and the countdown. See Phase 1. - Phase 2: history in Postgres, cooldown, heartbeat-driven release, metrics/Grafana, and polish (avatar choice, ETA).