Denis Cercasin
Design decisions
Table of contents
- D1 — Extend the existing codebase instead of rebuilding
- D2 — Migration from SQLite to PostgreSQL
- D3 — Idempotent startup migrations instead of a migration tool
- D4 — Patient profiles as data, not code
- D5 — Central weighted error catalog + rule-based evaluation engine
- D6 — WebSockets for the frontend, polling kept for Unity
- D7 — Zod validation at the API boundary
- D8 — Passwordless e-mail authentication (kept from 1.0)
- D9 — Patient file as an interactive VR object
- D10 — Inactive errors rendered in corrected state
- D11 — Desktop fallback mode
- D12 — Accessibility: color-vision modes in the frontend
- D13 — Documentation as GitHub Pages (Jekyll + just-the-docs)
- Information sources
- Missing information
This page records the significant decisions of Room of Horror 2.0 — what we chose, what we considered, and why. Decisions inherited from 1.0 that we consciously kept are marked as such.
D1 — Extend the existing codebase instead of rebuilding
Decision: Build 2.0 on top of the inherited 1.0 application (Unity scene, Express API, React frontend) rather than starting fresh.
Why: The 1.0 system was functional and battle-tested with the Meta Quest 3; the semester timeline made a rewrite unrealistic; the client’s priority was new capability (diversity, evaluation) — not new plumbing. Trade-off: we inherited conventions we would not have chosen (query-parameter APIs, promise-chain style, no tests) and spent significant time on code analysis first (see Lessons Learned: “existing code brings its own challenges” — „Bestehender Code bringt besondere Herausforderungen mit sich”).
D2 — Migration from SQLite to PostgreSQL
Decision: Replace SQLite with PostgreSQL, accessed through a single pg pool; UUID columns use the native UUID type, flexible structures use JSONB.
Why: SQLite’s single-writer model does not fit a server receiving concurrent requests from headsets and browsers; PostgreSQL adds native UUIDs, JSONB queries/validation, and real foreign keys with ON DELETE CASCADE. The migration also unlocked the normalized game-detail tables (game_error_results etc.) used by the evaluation reports. Configuration follows the standard DATABASE_URL / PG* variables, so any hosting environment works. (Agreed with the stakeholders in week 4; completed in week 6.)
D3 — Idempotent startup migrations instead of a migration tool
Decision: createTables() runs at every API start: CREATE TABLE IF NOT EXISTS, ADD COLUMN IF NOT EXISTS, guarded DO $$ blocks, seed upserts.
Why: For a small team without a shared migration workflow, “the running code defines the schema” removes an entire class of onboarding problems — a new developer needs only an empty database. Trade-off: no rollback and no schema history; acceptable at this project size (documented as an improvement for later).
D4 — Patient profiles as data, not code
Decision: Patient profiles live in the patients table (demographics, allowed error IDs, Unity model key, accent color), seeded at startup and served via GET /patient/get-patients. The frontend renders whatever the API returns; Unity switches models by the unity_model_id string.
Why: Diversity was the project’s core requirement — profiles had to be extensible by future teams without touching three codebases. A new patient needs: one seed entry, one Unity scene object, one avatar image. The backend validates selected errors against the patient’s allowed_error_ids, so the coupling “patient ↔ plausible errors” is enforced server-side, not by UI convention.
D5 — Central weighted error catalog + rule-based evaluation engine
Decision: All 32 errors are defined once in evaluation/ErrorCatalog.ts with category, severity weight (1–5), critical flag, and feedback hints; mirrored into error_definitions. A deterministic EvaluationEngine computes a weighted score, performance level, pass/fail, and generated feedback; results are versioned (rule-based-v1) in game_evaluations.
Why: The stakeholders required error prioritization (“higher-priority errors first”, week 6) and an evaluation the trainee can learn from. A rule-based engine (instead of the originally discussed AI evaluation) is explainable, testable, and free of external dependencies — important for an exam-adjacent context. Versioning keeps historical results comparable if the rules change. The AI variant was deliberately de-scoped (see Requirements Catalog) and documented as a feasibility study for future teams.
D6 — WebSockets for the frontend, polling kept for Unity
Decision: Add a Socket.IO layer (rooms per game, typed events) consumed by the frontend; Unity keeps its 1-second HTTP polling.
Why: The instructor dashboard needs instant feedback (error found, mistake made) — polling from the browser was wasteful and laggy. On the Unity side, polling was already reliable on the Quest, and swapping it for a socket client would have risked the stable VR build near the deadline (“stability before feature count” — „Stabilität vor Funktionsumfang”). The event contract already defines a vr client type so future teams can complete the migration.
D7 — Zod validation at the API boundary
Decision: Every endpoint validates query parameters and headers against a Zod schema before the handler runs (schemas/requests.ts + validate middleware).
Why: The inherited handlers mixed validation with business logic; malformed input could reach the database layer. Central schemas give uniform 400 responses, self-documenting request shapes, and one place to tighten rules (e.g. get-game’s “token or deviceUUID” rule as a superRefine). (Introduced together with UUIDs in week 4.)
D8 — Passwordless e-mail authentication (kept from 1.0)
Decision: Keep the inherited passwordless flow: e-mailed auth links, short-lived auth tokens exchanged for long-lived access tokens, one-time session-ID handover, token blacklist, recovery by auth_uuid rotation.
Why: No passwords to store or leak; instructors log in rarely, so link-based login is acceptable friction. DEV_MODE=true returns tokens in the response, which keeps local development mail-server-free. Trade-off: SMTP is a hard production dependency and long-lived access tokens increase the value of a stolen token (mitigated by blacklist and rotation).
D9 — Patient file as an interactive VR object
Decision: Implement the digital patient record as a clickable clipboard inside the VR room (PatientFileUI), with paging, per-patient content, and decision popups (translator, missing address, allergy conflict) — rather than showing patient data only in the web frontend.
Why: Reading the file is part of the nursing workflow being trained; several errors (26, 27, 28, 30) only make sense as documentation errors discovered in the record. Decision popups with exactly one professionally correct answer turn soft skills (communication, organization) into scoreable game events. (Requested by the stakeholders in week 6: file directly in VR, clickable, individual per patient.)
D10 — Inactive errors rendered in corrected state
Decision: Error objects that are not part of the current round are not simply hidden — correction components (MoveErrorContainer, VisualStateCorrection, TransformStateCorrection) display them in their correct state.
Why: If unselected error objects vanished, players could deduce active errors from missing furniture (“the urine bottle is gone, so it’s not a fault this time”). Rendering the corrected state keeps the room complete and forces genuine inspection.
D11 — Desktop fallback mode
Decision: Ship mouse/keyboard controls (DesktopMovement, DesktopClickInteractor, …) alongside VR input.
Why: Development, debugging, demos, and CI-less manual testing must not require a headset per developer; presentations need a projector-friendly mode. It also lowers the barrier for future contributors without VR hardware.
D12 — Accessibility: color-vision modes in the frontend
Decision: A global color-vision control (protanopia, deuteranopia, tritanopia, achromatopsia, standard — shown with German UI labels) persisted in web storage and applied as a data-color-vision attribute that CSS themes react to.
Why: Accessibility was a stakeholder requirement; a CSS-variable approach covers the whole UI at once instead of per-component fixes, and the choice survives reloads. VR-side measures (motion-sickness reduction) were addressed through teleport locomotion — the standard XR comfort technique — rather than smooth locomotion.
D13 — Documentation as GitHub Pages (Jekyll + just-the-docs)
Decision: All project documentation lives in docs/ as Markdown, published with Jekyll’s just-the-docs theme, with Mermaid for diagrams.
Why: Versioned together with the code, reviewable in PRs, zero hosting cost, and searchable navigation out of the box. Mermaid keeps diagrams editable as text instead of binary exports.
Information sources
- Repository code as referenced per decision (API
evaluation/*,schemas/*,websocket/*,database/General.ts; UnityAssets/Scripts/*; frontendsrc/components/ColorVisionControl.tsx) - Weekly Reports (stakeholder agreements in weeks 4 and 6)
- Requirements Catalog (scope decisions)
- External references: see Design Decisions Sources
Missing information
- No formal ADR (Architecture Decision Record) log was kept during development; decision dates beyond the weekly reports are approximate.
Last build: 19 Aug 2026, 05:20+00:00