A collaborative technical interviewing platform that puts video, a live-synced code editor, a shared whiteboard and proctoring in one screen — with scheduling, feedback and shortlisting built in.
Rooms are created with a random UUID that doubles as the join key between Stream and the database. Tokens are minted in a server action so the Stream secret never reaches the browser. Only the room creator can end the call, which also flips the interview to completed.
Every participant sees the same code, language and question in real time through a reactive subscription. Code runs through a compile API with pinned compiler builds for six languages, and the output pane normalises compile errors, stdout, stderr and signals into one view.
A 1,000-line hand-rolled HTML5 canvas with select, pencil, eraser, line, rectangle, circle and text tools, an eight-colour palette, branching undo/redo, drag-to-move with hit-testing, and full mouse and touch support. Strokes sync to all participants after each commit.
Candidates with more than one display are blocked at the pre-call screen, and leaving fullscreen mid-interview fires an event the interviewer sees instantly. Detection falls back across three browser APIs so it works beyond Chrome.
A calendar and 5-minute time picker, candidate selection filtered by role, multi-select co-interviewers with the scheduler pinned, and end-before-start validation. Scheduling creates the Stream room and the interview record in one flow.
Interviewers leave 1–5 star ratings with comments per interview, build their own problem bank with examples and constraints on top of nine built-in questions, and move candidates through upcoming → completed → succeeded or failed from the dashboard.
Next.js 14 App Router on the front, Convex as a reactive database and function layer, Clerk for identity and Stream for WebRTC. There is no separate server: every realtime feature is a Convex row that clients subscribe to, keyed by the Stream call id.
Interview integrity depends on knowing whether the candidate has a hidden screen, but there is no single reliable browser API for screen count. Chrome has an experimental Window Management API, other browsers expose only a boolean, and some expose nothing.
A three-tier fallback: ask getScreenDetails() for an exact count, then check screen.isExtended (handling the browsers where it is a Promise), then fall back to an aspect-ratio heuristic where width/height above 2.5 almost always means an extended desktop. The hook fires transitions in both directions so the interviewer sees when a second screen is connected or removed mid-call.
Proctoring events originate on the candidate and must reach the interviewer without a custom websocket server. The Stream SDK supports custom events, but its payload shape differed between versions and nesting levels.
Reused the existing call as the transport. The candidate sends typed custom events, and the interviewer handler defensively probes several payload shapes before trusting one. Fullscreen re-entry cannot be automated without a user gesture, so a blocking modal explains and waits for the click.
Two people typing into the same Monaco instance will fight each other if every remote update replaces the local buffer, and naïve syncing loops updates back to the sender.
Chose last-write-wins on a single row per call, which is the right trade-off for an interview where one person types at a time. Each mutation stamps updatedBy, and clients skip updates they authored. Language and question selection ride on the same row so a switch by either side is reflected everywhere.
Off-the-shelf whiteboards were heavy and hard to embed inside a tab next to a code editor, and none synced through Convex cleanly.
Built an immediate-mode renderer over an element array: hit-testing for select and drag, a lasso-style eraser, branching undo/redo that truncates the future on a new stroke, inline text entry positioned over the canvas, and unified pointer/touch mapping via getBoundingClientRect. The element array is serialised to Convex on each committed stroke.
Each language returns a different mix of compiler output, program output, stderr and signals, and the service compiles Java under a fixed filename that breaks the usual public class Main convention.
Pinned exact compiler build ids per language, rewrote public class to class for Java before submission, and merged the five output fields into one ordered string with a friendly fallback when the program prints nothing.