Onboarding Gate Flow
The 8-step onboarding gate every fresh install must clear before the launcher renders. Order is load-bearing: each step either sets state the next ones depend on, or enforces a compliance boundary.
Flow
| Step | Category | Sets | Hard-blocks on |
|---|---|---|---|
| 1 Age + Country | Gate | ageConfirmed, country | Checkbox off, no country |
| 2 Welcome / Language | Gate | language (en / it) | n/a — click-to-advance |
| 3 Video | Gate | — | n/a — SKIP and CONTINUE both advance |
| 4 Legal | Compliance | consents.tos, .privacy, .eula | Any of 3 checkboxes off |
| 5 EDC Consent | Compliance | consents.edcGameplay | Gameplay toggle off or checkbox off |
| 6 Connect Steam | Identity | oauth.steam.{status,username}, OTEL host.username | Steam OAuth never completes |
| 7 System Check | Diagnostics | SystemCheckReport (telemetry only) | Any entry with severity: blocking |
| 8 Ready / GO | Done | playroll.onboardingV3Complete = true | n/a — single button advance |
Why this order
- Compliance before identity. Legal + EDC must be accepted before any account is linked, so consent records have an
acceptedAttimestamp that predates the Steam ID being on file. - Identity before diagnostics. Steam OAuth at step 6 calls
otelLogger.setUserContext({ ownerId, username }). Every diagnostic event emitted by step 7 is then automatically tagged withhost.username+enduser.id, so opaque failures land on SigNoz already attributed to a known tester instead of an anonymous device. - Diagnostics before launcher. Hardware-level blockers (no GPU encoder, NVIDIA driver too old, disk full, OS too old) are surfaced before the first recording attempt, with action buttons that resolve them in one click.
Step 7 — System Check
Five probes, three severities. CONTINUE is gated on hasBlockers === false.
| Check | Source | Thresholds | Severity ladder |
|---|---|---|---|
| OS | os.release() | Win10 build 18362 / 20348 | block < 18362 · warn < 20348 · ok |
| CPU | os.cpus().length | 8 / 12 threads | warn < 8 · ok |
| RAM | os.totalmem() | 8 / 16 GB | block < 8 · warn < 16 · ok |
| Disk | statfs(recordingsPath) | 10 / 50 GB free | block < 10 · warn < 50 · ok |
| GPU | /api/environment/{check,adapter} (playroll-core) | NVIDIA + CUDA, driver ≥ 570 (NVENC + on-device transcription) | block on AMD-only / Intel-only systems (even with a working AMF / QuickSync encoder), or NVIDIA encoder unusable · warn driver old · ok |
Thresholds mirror the Notion 🖥️ System Requirements page. Update both in lockstep when a build shifts a floor.
The GPU row is intentionally stricter than a generic "any working encoder" check. The cpp recording pipeline has been NVENC-only since EC-173, and EC-260..EC-264 added a CUDA-only on-device transcription pipeline. The onboarding gate reflects that single answer: anything other than an NVIDIA + CUDA setup blocks the user before they can open the launcher. The hard block lives in src/main/system-check/checkers.ts::checkGpu and is unit-tested by tests/main/system-check.test.ts.
Voice gates within Step 7 (EC-306 / EC-317)
Beyond the five hardware probes, Step 7 carries three additional gates for the voice stack, each a prerequisite of the next. They only apply when transcription is applicable (a supported recommended model exists — always true on this NVIDIA-only product) and the core is reachable; when the system check is unavailable (core down) none of them trap the user.
- Microphone check (
MicCheckRow, EC-306) — record a 5 s clip, play it back, confirm. Software signal check (peak ≥ 0.02) plus manual confirmation. Gates the model download below. - Model download (
TranscriptionCheckRow, EC-265) — engine (CUDA backend DLLs) + Whisper model fetched once, so the launcher opens with a working stack. Gated on the mic check; gates the transcript self-test below. - Transcript self-test (
TranscriptConfirmRow, EC-317) — transcribe the user's own mic-test clip viaPOST /api/transcribe/fileand have them confirm the text reads correctly. This is the deterministic "transcription works on this PC" verdict — a human in the loop — and exercises the ggml/CUDA path that has historically failed silently (EC-304 stale DLL, EC-309 VRAM, FA-on-Blackwell). It is a hard gate, with recovery paths so a genuine local failure never traps the user: re-record (clears the confirm), Retry, or an explicit, telemetry'd "Skip — transcription unavailable on this PC". Confirmation / skip / failure each emitonboarding.transcript_selftest.*telemetry.
CONTINUE gates on hasBlockers === false and all three voice gates (micReady && transcriptionReady && transcriptReady).
Telemetry — attributed by design
Steam OAuth at step 6 primes setUserContext in otel-logger.ts, so step 7 events arrive on SigNoz with host.username already attached.
| Event | When | Level | Key attributes |
|---|---|---|---|
onboarding.system_check.result | every run (auto + retry) | mapped from overall | outcome, has_blockers, gpu_probe_source, entries.{count,blockers,warnings}, run_trigger |
onboarding.system_check.entry | per non-ok entry | mapped from severity | entry.id, entry.severity, entry.measured, entry.required, entry.action_hint |
onboarding.system_check.unavailable | IPC missing / crash | warn / error | reason (ipc_missing / ipc_error), error.message |
ClickHouse triage queries:
-- testers blocked at GPU step with NVIDIA driver too old
SELECT host.username, system_check.entry.measured
FROM otel_logs
WHERE event.name = 'onboarding.system_check.entry'
AND system_check.entry.id = 'gpu'
AND system_check.entry.action_hint = 'update_nvidia_driver';
-- block-rate per check id
SELECT system_check.entry.id, count() AS blocks
FROM otel_logs
WHERE event.name = 'onboarding.system_check.entry'
AND system_check.entry.severity = 'blocking'
GROUP BY system_check.entry.id;
State surface
OnboardingState is held in a useReducer and flushed to GateState via setGateState on completion. Local component state (checkboxes, toggles) resets on BACK; reducer state (language, country, age, consents, OAuth) persists across back-nav.
The consent records collected here (age + tos + privacy + eula + edcGameplay) are batch-submitted to the consents-accept edge function after step 6 (Steam login), so the rows land in playroll_user_consents with the authenticated user_id. The boot-time re-prompt logic (when we rotate a document under a material change) lives in Consent Versioning.
| Key | Type | Set at | Persistence |
|---|---|---|---|
playroll.onboardingV3Complete | boolean | Step 8 | GateStore — gates re-entry to onboarding |
playroll.language | "en" | "it" | Step 2 | GateStore — app-wide locale |
playroll.profile.country | CountryCode | Step 1 | GateStore |
playroll.consents.{tos,privacy,eula,edcGameplay} | {acceptedAt} | Steps 4–5 | GateStore — audit trail |
oauth.steam.{status,username} | reducer-only | Step 6 | persisted via AuthManager, not GateStore |
SystemCheckReport | reducer-only | Step 7 | telemetry only, not persisted |
Pre-launch posture
v3 was designed for fresh installs. There is no migration path from any prior onboarding version — every device sees v3 from a clean slate on first run. Existing v2 frames remain on the Paper canvas marked DEPRECATED — DO NOT USE for archive only.
References
- UI:
src/renderer/features/onboarding/—steps.ts,OnboardingFlow.tsx,steps/SystemCheckStep.tsx - Main:
src/main/system-check/— checkers, thresholds, IPC handler - Core:
src/service/env_check.cpp,src/service/http_server.cpp(/api/environment/check,/api/environment/adapter) - Linear: EC-184 (UX-recoverable technical bugs), EC-185 (disk), EC-186 (NVIDIA driver)