
Real-Time Technical Interview Platform
I built a platform for running technical interviews end to end — scheduling, live video, collaborative coding, and structured feedback. The interesting problem was never the CRUD; it was running a stranger's code on my own server without handing them the host. Here's how the pieces fit together.
Five roles — candidate, interviewer, recruiter, developer, admin — drive everything from routing to which dashboard you land on.
The core feature is a Monaco editor where candidates write and run JS, Python, or Java live. That means executing untrusted input on
every "Run". Because the app talks to the host Docker daemon over a mounted socket, I deliberately don't bind-mount the candidate's
code — I pipe it in over stdin, so there's no host path a container can reach back into.
Every run is an ephemeral container locked down hard:
docker run --rm -i \
--network none \ # no network at all
--memory 128m --memory-swap 128m \ # capped, swap disabled
--cpus 0.5 --pids-limit 64 \
--cap-drop ALL --security-opt no-new-privileges \
--read-only --tmpfs /tmp:size=32m \
<runner-image>
# plus a 10s wall-clock timeout and capped output sizeThe runner also separates failure modes deliberately: a broken sandbox returns 503 (our problem), while a candidate's syntax error or crash returns 200 with the captured output (their code's problem). The UI never confuses "your code failed" with "our platform failed".
Making the socket usable without running as root took a build-time trick: the Dockerfile installs the docker CLI and matches the container's docker group GID to the host's at build time — falling back gracefully on a GID collision — so the app runs as a non-root user but can still reach the daemon.
Access control runs before the page does. Clerk middleware resolves the signed-in user, then queries Convex for their role and
enforces route access in middleware.ts, injecting a correlation ID for tracing along the way. Unauthorized roles are redirected before
a protected page ever renders — no reliance on scattered per-page checks.
Video, calls, and recordings run on Stream, brokered through server actions so tokens never reach the client. Hosts get an explicit "end meeting" action, and recordings are retained per interview under a retention policy. Interviewers evaluate candidates with weighted scorecards — shared vs private notes, and pass / reject / hold / review outcomes — which feed a hiring-pipeline workspace with a funnel and filters.
A lot of the work went into the boring, load-bearing parts:
next build so the
image builds without secrets present.convex export, prunes by retention, persists to a named volume, and
reports each snapshot back to the app over an authenticated internal endpoint.CI runs tsc --noEmit and the unit suite on every push. The deploy workflow builds an arm64 image on a native ARM runner, pushes it to
a container registry, and the stack runs from there as two services — app plus the backup sidecar — behind a reverse proxy and gated on
an /api/health check.
The honest gap is test coverage: there's a small unit suite (error handling, feature flags, rate limiting) but no component or end-to-end tests yet. Given how much of the product's value lives in the live interview flow, that's the next thing I'd build out.