cat projects/intervuex.md
    Live
    Next.js 14
    Convex
    Clerk
    Stream

    Intervue X

    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.

    2025 · Personal project Solo build — design, backend, frontend, deploy
    3
    Workflows unified
    Video · code · canvas
    6
    Languages executed
    JS, TS, Python, Java, C++, Go
    19
    Realtime backend functions
    11 queries · 8 mutations
    6
    Data tables
    7 indexes
    2
    Roles
    Interviewer · candidate
    4
    Interview states
    Upcoming → completed → pass/fail

    Screens

    interviewer dashboard
    interviewer dashboard
    live meeting — video + synced editor
    live meeting — video + synced editor
    shared whiteboard
    shared whiteboard
    custom problem bank
    custom problem bank

    What I built

    Video interviews on Stream

    Server-minted tokens

    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.

    Live-synced Monaco editor + 6 compilers

    6 languages

    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.

    Shared whiteboard, built from scratch

    7 tools · touch-ready

    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.

    Proctoring: fullscreen + multi-monitor detection

    3-tier detection

    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.

    Scheduling with co-interviewers

    Multi-interviewer

    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.

    Feedback, custom problems & shortlisting

    9 built-in + custom

    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.

    How it's built

    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.

    Frontend
    • Next.js 14 App Router with route groups for interviewer, candidate and admin views
    • Monaco editor in a 3-tab shell: code · output · canvas
    • Two layout trees: resizable 25/75 panels on desktop, video carousel over a 70% editor on mobile
    • Tailwind + Radix primitives, react-day-picker for scheduling
    Realtime data (Convex)
    • 6 tables: users, interviews, comments, codeStates, customProblems, canvasStates
    • One codeStates and one canvasStates row per call, patched in place and pushed to all subscribers
    • Interviews indexed by candidate and by Stream call id
    • Mutations stamp updatedBy so a client can ignore its own echo
    Identity (Clerk)
    • Clerk JWT template verified by Convex; identity.subject is the user key everywhere
    • Svix-verified webhook syncs new users into Convex on user.created
    • Client-side fallback sync so the app works even without the webhook configured
    • Users pick interviewer or candidate once; role drives routing and dashboards
    Video & execution
    • Stream Video SDK; rooms created via getOrCreate with a UUID stored on the interview
    • Custom Stream events carry proctoring signals from candidate to interviewer
    • POST /api/run proxies to a compile service with pinned builds (Node 20, TS 5.6, CPython 3.13, JDK 22, GCC 13, Go 1.23)
    • Java sources rewritten so they compile under the service's fixed filename
    cat docs/request-flow.txt
    1. 1.Interviewer schedules → Stream call getOrCreate(uuid) → Convex createInterview({ streamCallId: uuid })
    2. 2.Candidate opens /meeting/[id] → server action mints Stream token → pre-call proctoring check runs
    3. 3.Both join the call; CodeEditor subscribes to codeStates by streamCallId
    4. 4.Keystroke → upsertCodeState → Convex pushes the row → peers apply it unless updatedBy is themselves
    5. 5.Run → POST /api/run → compile service → normalised output rendered in the output pane
    6. 6.Candidate exits fullscreen → call.sendCustomEvent → interviewer sees a live warning
    7. 7.Interviewer ends call → status: completed → rates + comments → marks succeeded / failed

    Hardest problems

    Detecting a second monitor across browsers

    Problem

    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.

    How I solved it

    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.

    src/hooks/useProctoring.ts

    Signalling proctoring events through a video SDK

    Problem

    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.

    How I solved it

    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.

    src/components/MeetingRoom.tsx

    Realtime code sync without a CRDT

    Problem

    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.

    How I solved it

    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.

    convex/code.ts · src/components/CodeEditor.tsx

    A collaborative whiteboard with no canvas library

    Problem

    Off-the-shelf whiteboards were heavy and hard to embed inside a tab next to a code editor, and none synced through Convex cleanly.

    How I solved it

    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.

    src/components/canvas.tsx

    Six languages through one compile endpoint

    Problem

    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.

    How I solved it

    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.

    src/app/api/run/route.ts

    What I'd do differently

    • $Debounce editor mutations. Every keystroke currently writes a row; batching at ~100ms would cut writes by an order of magnitude with no visible latency.
    • $Move role checks server-side. Role gating is enforced in the client today; Convex functions should reject calls from the wrong role too.
    • $Self-host code execution in an isolated container with a timeout and rate limit instead of relying on a public compile service.
    • $Adopt a CRDT such as Yjs if the product ever needs true simultaneous editing rather than turn-taking.

    Tech stack

    Next.js 14
    TypeScript
    Convex
    Clerk
    Stream Video
    Monaco Editor
    Tailwind CSS
    Radix UI
    HTML5 Canvas
    $ cd ../portfolio