CAPNO Studio is an open, installable web app for running realistic anesthesia scenarios in a simulation lab. Faculty drive a simulated patient monitor in real time — vitals, rhythms, and prebuilt clinical events — while students follow on CAPNO Rounds, a full-screen OR-style monitor for a projector, wall display, or iPad. Scenarios come from CAPNO Cases, the built-in case library and editor, and every session ends in CAPNO Debrief — a structured, printable report with a timeline, action assessment, scoring rubric, and suggested debrief questions.
⚠️ For simulation and education only. Not a medical device. Never use for clinical care.
- 🎛️ Faculty controller — start/pause/reset, live sliders for HR, BP, SpO₂, EtCO₂, RR, temperature, anesthetic depth and agent, rhythm selection, one-tap scenario events, phase-of-care stepper, timestamped notes, and a learner-action checklist (done / delayed / missed / incorrect). A glanceable script rail shows what's next — automatic events with live countdowns (flashing when imminent) and upcoming manual events in narrative order; tap any chip to fire it early (which cancels its scheduled copy).
- 🫀 Student monitor — sweep-style ECG, plethysmograph and capnograph waveforms rendered on canvas, numeric tiles with trend arrows, and conventional alarm limits with warning/critical states. Optional monitor audio (Web Audio, no deps): per-beat pulse tone whose pitch falls with SpO₂, plus a critical-alarm tone that respects faculty silencing. NIBP behaves like a real cuff — readings update on a cycle interval (with on-demand "cycle now"), and BP alarms judge the last measured value; scenarios can opt into an arterial line for continuous pressure.
- 🧠 Scenario engine — a framework-free, tick-driven state machine with linear vital ramps, delayed/automatic events, phases, expected actions, and scoring. Fully unit-tested.
- 📚 Starter library — five polished scenarios: post-induction hypotension, laryngospasm after LMA, intraoperative anaphylaxis, malignant hyperthermia, and LAST after a peripheral nerve block. Each includes objectives, setup, patient background, progression, triggers, correct management, common errors, a debrief guide, and a rubric.
- ✏️ Scenario editor — form-based editing of everything including phases, events with vital effects, expected actions, and the grading rubric, plus a live JSON pane for power users, zod validation, import/export of scenario files, version history, and tags by topic/difficulty/training level.
- 📋 Debrief & assessment — timeline of everything that happened, a vitals trend strip (10-second samples with event markers — "SpO₂ began falling here, the team responded here"), actions taken vs. missed critical actions, category scores, faculty notes, suggested questions, and one-click PDF export. Action statuses can be amended post-hoc with automatic rescoring — marking reliably mid-crisis is unrealistic — and learner names can be added to the printed report. Faculty can also skip uneventful scenario time (+1/+5 min). Session archives export/import as JSON for backup or moving between machines.
- 📱 PWA — installable on iPad/laptop, offline-capable: bundled scenarios and previously-visited views work with no connectivity.
- 🔌 Realtime sync — works with zero configuration on one device (controller window + student window/projector), with a join timeout, a stale-connection watchdog on the student display, and a Local/Cloud sync-health indicator. Add Supabase for cross-device sync (iPad controller → projector PC).
- 🏫 Institutional layer (optional) — with Supabase configured, faculty sign in at
/account(email/password); custom scenarios and session debriefs then sync to institution-wide storage through an offline-first outbox (local saves always succeed; pushes queue and drain when online). The debrief page gains a read-only institution archive across devices.
npm install
npm run dev
# open http://localhost:3000Typical lab flow:
- Open Faculty Controller → pick a scenario → Run. Note the 4-character session code.
- On the projector/student device, open Student Display and enter the code.
- Start the scenario. Drive vitals with the sliders, fire events, mark learner actions, add notes.
- End session → the debrief report opens. Export PDF uses the browser print dialog.
With no backend configured, the student display must be a second window/tab of the same browser on the same machine (BroadcastChannel transport) — which covers the common "laptop + projector" lab setup. For separate devices, configure Supabase (below).
| Guide | For |
|---|---|
| docs/faculty-guide.md | Instructors — room setup, connecting the student display, driving vitals/events, assessing learners, debrief and PDF export, scenario authoring basics, troubleshooting. |
| docs/student-guide.md | Learners / projector operators — joining with the session code, reading the monitor, NIBP vs arterial line, alarms and sound. |
| docs/DEPLOYMENT.md | Admins — Vercel, self-hosted Node, offline lab machines, Supabase setup, PWA installation. |
| docs/scenario.schema.md | Scenario authors — the full scenario JSON field reference. |
Copy .env.example to .env.local. Everything is optional:
| Variable | Purpose |
|---|---|
NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY |
Enables cross-device realtime sync (Supabase Realtime broadcast), institution sign-in at /account, and cloud persistence of custom scenarios and session debriefs for faculty accounts. Run db/schema.sql in your Supabase project and enable the Email auth provider (see docs/DEPLOYMENT.md). |
NEXT_PUBLIC_FACULTY_PIN |
Advisory PIN gate on faculty views for shared lab machines. Not a security boundary (it is a public client value) — use Supabase auth + RLS for real access control. Signed-in faculty/admin accounts bypass the PIN. |
src/
lib/
engine/ # Apache-2.0 core: pure TS, no framework imports
types.ts # scenario + runtime types (single source of truth)
schema.ts # zod validation for scenario JSON (editor, import, registry)
engine.ts # SimulationEngine: tick-driven state machine, vital ramps
vitals.ts # display metadata, clamping, alarm evaluation
scoring.ts # rubric scoring from marked actions
sync/ # realtime transports behind one SyncChannel interface
broadcast.ts # BroadcastChannel (default: same-device, offline)
supabase.ts # Supabase Realtime broadcast (cross-device, optional)
store/ # zustand stores
controllerStore.ts # faculty side: owns the engine, ticks, broadcasts snapshots
studentStore.ts # student side: read-only snapshot mirror + liveness watchdog
sessionArchive.ts # completed sessions in localStorage (debrief input)
sessionExport.ts # JSON export/import of session archives
cloud/ # optional Supabase layer (auth, offline-first outbox,
# scenario + session cloud sync) — no-ops when unconfigured
scenarios/ # built-in registry + custom scenarios (localStorage, versioned)
scenarios/*.json # the starter scenario library (validated at load)
components/
monitor/ # waveform canvas, vital tiles, alarms, full monitor
controller/ # session, vitals, events, actions, phases, notes, log panels
debrief/ # printable report
editor/ # scenario editor
app/ # Next.js routes: /, /student, /scenarios, /faculty/run/[id],
# /debrief, /debrief/[id], /debrief/cloud/[id], /editor, /account
db/schema.sql # optional Supabase schema: roles, scenarios, sessions, RLS
public/sw.js # offline service worker
- The faculty controller is authoritative. The engine runs only on the faculty device; student displays are dumb mirrors of broadcast snapshots. Full-state replication (a few KB, twice a second) makes late joins, refreshes, and dropped messages self-healing — a display is always correct within one tick.
- Realtime is an adapter, not a dependency.
SyncChannelhas two implementations: BroadcastChannel (free, offline, same device) and Supabase Realtime broadcast (cross-device). A WebSocket server adapter can be added without touching UI code. - Scenarios are data. Plain JSON validated by one zod schema shared by the registry, the editor, and file import. Built-ins are statically bundled — which is what makes them available offline with zero fetches.
- PDF export is the browser's print engine. The debrief report has dedicated print CSS; "Export PDF" opens the native dialog. Zero heavy dependencies, works offline, and produces clean, paginated documents.
- Open-core split. Everything under
src/lib/engineis deliberately framework-free and Apache-2.0. Premium institutional features (hosted accounts, cross-session analytics, LMS export) layer on via the database schema and would live in separate packages/services, not in the engine.
See docs/scenario.schema.md for the full field reference, or export any built-in scenario from the library as a working example. Minimal shape:
npm run dev # dev server
npm test # vitest unit tests (engine, scoring, schema + scenario validation)
npm run typecheck # tsc --noEmit
npm run lint # next lint (ESLint)
npm run build # production build
npm run icons # regenerate PWA icons (scripts/gen-icons.mjs, no deps)See docs/DEPLOYMENT.md — covers Vercel, self-hosted Node, static lab machines, Supabase setup, and PWA installation on iPad.
- No PHI: Capno stores simulated patients only. Learner names on debriefs are optional free text entered by faculty — treat them per your institution's policy.
- All scenario input is schema-validated; imported files and cloud-fetched definitions never execute code and pass the same zod boundary.
- Realtime messages are ephemeral broadcast state; nothing per-tick is persisted.
- The faculty PIN is an advisory gate for shared machines, not authentication. Real role-based access (student/faculty/admin) is Supabase auth (email/password sign-in at
/account) + the RLS policies indb/schema.sql; new accounts default tostudentand only admins can promote. - No analytics, tracking, or third-party requests in the core app; the only external endpoint is your own Supabase project, when configured.
- Server-authoritative session host (WebSocket adapter) for multi-room installations
- Learner self-service session review; LMS (LTI) integration
- Vital-sign physiology models (drug PK/PD, baroreflex) layered on the ramp engine
- Waveform artifact library (motion, electrocautery, disconnects) and audible alarms/beep tones
- Multi-monitor layouts (anesthesia machine screen, ventilator loops, agent analyzer)
- Scenario branching graphs with guarded transitions (currently: phases + faculty judgment)
- Cross-session analytics dashboards per cohort/institution (premium)
- Scenario marketplace with peer review (premium/hosted)
Core: Apache-2.0. The five starter scenarios are provided under the same license for academic use. Institutional/hosted features are intended to live outside this repository.
© 2026 Capno Labs LLC · Apache-2.0
{ "id": "my-scenario", "version": "1.0.0", "title": "…", "summary": "…", "tags": { "topics": ["airway"], "difficulty": "beginner", "trainingLevels": ["srna"] }, "learningObjectives": ["…"], "setup": ["…"], "patient": { "name": "…", "age": 55, "sex": "male", "weightKg": 80, "heightCm": 175, "asa": 2, "allergies": [], "medications": [], "pmh": [], "airway": { "mallampati": 2 } }, "baselineVitals": { "hr": 72, "sbp": 120, "dbp": 75, "spo2": 98, "etco2": 36, "rr": 14, "temp": 36.6, "depth": 95, "agentEt": 0, "agentFi": 0, "rhythm": "sinus" }, "phases": [{ "id": "induction", "label": "Induction" }], "events": [{ "id": "hypotension", "label": "Hypotension", "category": "circulation", "effects": [{ "vitals": { "sbp": 72, "dbp": 40 }, "overSec": 90 }] }], "expectedActions": [{ "id": "give-pressor", "label": "Administer vasopressor", "phase": "induction", "critical": true, "points": 20 }], "expectedProgression": ["…"], "correctManagement": ["…"], "commonErrors": ["…"], "debrief": { "points": ["…"], "questions": ["…"] }, "rubric": [{ "id": "management", "label": "Management", "actionIds": ["give-pressor"] }], "estimatedMinutes": 15 }