Voice-first AI mock interviews. Talk naturally, get real-time follow-up questions, and finish with a detailed report that shows exactly how to improve.
- Real-time voice interviews β Fully spoken, back-and-forth conversation with an AI interviewer across 4 formats (Behavioral, Technical, System Design, HR), each with its own persona and rubric.
- Adaptive questioning β A decision engine reads every answer and adjusts live: weak answer β follow-up probe, strong answer β harder question, then wraps up once the rubric is covered.
- Resume + JD personalization β Add your resume and paste the target job description; the interviewer asks about your real projects for that specific role, and the report includes a resume-vs-interview gap analysis.
- Deep scored feedback report β Overall score /100, hiring-manager verdict, top-3 fixes, per-competency 1β5 breakdown with evidence, and a question-by-question review with a concrete "try this instead."
- Natural conversation UX β Tap-to-interrupt barge-in, an animated voice orb that reacts to speech, and objective delivery metrics (talk ratio, filler-word rate, avg words per answer).
| Layer | Choice |
|---|---|
| Frontend | React 18 (Vite), React Router |
| Backend | Node.js, Express, WebSocket |
| Database | PostgreSQL 16 (Docker), Prisma 6 ORM |
| Voice engine | Deepgram Voice Agent API: STT (Nova-3) + LLM + TTS (Aura-2) |
| Interview brain | LangGraph + Groq (gpt-oss-120b) for live scoring/routing, with heuristic fallback |
| Auth | JWT (jsonwebtoken) + bcryptjs |
End-to-end journey a candidate takes through the app.
flowchart TD
Start([Land on app]) --> HasAcct{Has account?}
HasAcct -- No --> Signup[Sign up: name, email,<br/>job role, experience]
HasAcct -- Yes --> Login[Log in]
Signup --> Dash
Login --> Dash
Dash[Dashboard: past interviews<br/>+ interview types]
Dash --> AddResume{Resume on<br/>profile?}
AddResume -- No, optional --> Profile[Add rΓ©sumΓ© + skills<br/>via Profile modal]
Profile --> Dash
AddResume -- Yes --> PickType
Dash --> PickType[Pick interview type:<br/>Behavioral Β· Technical Β·<br/>System Design Β· HR]
PickType --> Lobby[Lobby: review rΓ©sumΓ© status,<br/>optionally paste Job Description]
Lobby --> Begin[Begin β create/patch interview,<br/>grant mic]
Begin --> Live[Live voice interview<br/>speak with AI interviewer]
Live --> Talk{Candidate turn}
Talk -- Answer --> Live
Talk -- Tap to interrupt --> Live
Talk -- End button --> Finish
Live --> AutoEnd[Agent wraps up /<br/>soft nudge / hard cap]
AutoEnd --> Finish[Finalize: drain audio,<br/>POST /finish]
Finish --> Report[Feedback report:<br/>score ring, per-competency,<br/>STAR, strengths, growth, timeline]
Report --> Dash
Report --> Retry[Start another interview] --> PickType
Relational model as defined in backend/prisma/schema.prisma.
erDiagram
USER ||--o{ INTERVIEW : "has many"
INTERVIEW ||--o{ TRANSCRIPTION : "has many"
INTERVIEW ||--o{ ASSESSMENT : "has many"
INTERVIEW ||--o| FEEDBACK : "has one"
USER {
int id PK
string email UK
string password_hash
string name
string job_role
string experience_level
string resume_text "nullable"
string skills "nullable"
int years_experience "nullable"
timestamptz created_at
}
INTERVIEW {
int id PK
int user_id FK
string type "default behavioral"
string status "in_progress|completed|abandoned"
timestamptz started_at
timestamptz ended_at "nullable"
string deepgram_request_id "nullable"
string jd_text "nullable"
}
TRANSCRIPTION {
int id PK
int interview_id FK
int seq
string role "user|assistant"
string content
timestamptz created_at
}
ASSESSMENT {
int id PK
int interview_id FK
string competency
string topic "nullable"
int score
string note "nullable"
timestamptz created_at
}
FEEDBACK {
int interview_id PK "FK, one-to-one"
int overall_score "nullable"
string summary "nullable"
string verdict "nullable"
json top_priorities
json per_competency
json strengths
json growth_areas
json star
json timeline
json exchanges
timestamptz created_at
}
Cascade: deleting a User cascades to their Interviews; deleting an Interview cascades to its Transcriptions, Assessments and Feedback.
.
βββ docker-compose.yml # PostgreSQL 16, host port 5433
βββ backend/
β βββ prisma/schema.prisma # User, Interview, Transcription, Assessment, Feedback
β βββ src/
β βββ server.js # HTTP + WS upgrade (JWT + ownership check) entrypoint
β βββ app.js # Express app: /api/auth, /api/interviews
β βββ config/config.js # All env-driven config (voice, limits, graph, eval)
β βββ controllers/ # authController, interviewController
β βββ routes/ # authRoutes, interviewRoutes
β βββ services/
β β βββ voiceProxy.js # WS bridge to Deepgram, call-ending state machine
β β βββ functionHandlers.js # record_assessment / submit_evaluation tools
β β βββ transcriptEvaluator.js # post-call fallback scoring
β β βββ reportService.js
β βββ langGraph/ # state.js, nodes.js, interviewGraph.js, orchestrator.js
β βββ domain/interviewTypes.js # per-type competencies/topics/phases
β βββ prompts/interviewer.js # prompt builder (resume/JD threading)
β βββ middleware/verifyToken.js
βββ frontend/
βββ src/
βββ pages/ # Login, Signup, Dashboard, InterviewRoom, Report
βββ components/ # ProfileModal, VoiceOrb, Brand
βββ audio/ # recorder.js, player.js (PCM), sfx.js (chimes)
βββ styles/
- Node.js 18+
- Docker (for PostgreSQL)
- A Deepgram API key (required for the voice loop)
- A Groq API key (optional β enables live LangGraph scoring; heuristic fallback works without it)
# 1. Start PostgreSQL
docker compose up -d
# 2. Backend
cd backend
npm install
cp .env.example .env # fill in DEEPGRAM_API_KEY, JWT_SECRET, GROQ_API_KEY
npm run db:migrate # prisma db push
npm run dev # http://localhost:3000
# 3. Frontend (separate shell)
cd frontend
npm install
npm run dev # http://localhost:5173Set in backend/.env (see backend/.env.example):
| Variable | Required | Default | Purpose |
|---|---|---|---|
DEEPGRAM_API_KEY |
β | β | Voice Agent WS (STT+LLM+TTS) |
JWT_SECRET |
β | β | Signs the auth cookie |
DATABASE_URL |
β | postgres://interviewlab:interviewlab@localhost:5433/interviewlab |
Prisma connection |
GROQ_API_KEY |
optional | β | Enables live LangGraph node LLM calls (else heuristic fallback) |
EVAL_MODEL |
optional | openai/gpt-oss-120b |
Model for post-call fallback evaluation & graph nodes |
PORT |
optional | 3000 (4000 in code default) |
Backend port |
CLIENT_ORIGIN |
optional | http://localhost:5173 |
CORS origin |
GRAPH_DRIVEN |
optional | true |
Toggle LangGraph director vs. legacy autonomous prompt |
SOFT_WRAP_MS / POST_NUDGE_MS / MAX_DURATION_MS |
optional | 7min / 60s / 11min | Call-ending schedule (soft nudge β escalation β hard cap) |
JD_MAX_CHARS / RESUME_MAX_CHARS |
optional | 2000 / 3500 | Prompt-context clipping |
| Command | Where | Purpose |
|---|---|---|
docker compose up -d |
root | Start PostgreSQL |
npm run dev |
backend/ |
Start API + WS proxy with nodemon |
npm start |
backend/ |
Start API + WS proxy (no reload) |
npm run db:migrate |
backend/ |
prisma db push β sync schema to DB |
npm run db:dev |
backend/ |
prisma migrate dev β create a migration |
npm run db:deploy |
backend/ |
prisma migrate deploy β apply migrations (prod) |
npm run db:generate |
backend/ |
Regenerate Prisma client |
npm run dev |
frontend/ |
Start Vite dev server |
npm run build |
frontend/ |
Production build |
npm run preview |
frontend/ |
Preview production build |
Auth (/api/auth)
| Method | Path | Notes |
|---|---|---|
| POST | /signup |
Create account (email, password, name, jobRole, experienceLevel) |
| POST | /login |
Sets JWT cookie |
| POST | /logout |
Clears cookie |
| GET | /me |
Current user (auth required) |
| PATCH | /profile |
Partial update β resume text, skills, years of experience |
Interviews (/api/interviews, all auth-required)
| Method | Path | Notes |
|---|---|---|
| POST | / |
Create interview (type, optional jdText) |
| GET | / |
List current user's interviews |
| GET | /:id |
Fetch report / current state |
| PATCH | /:id |
Set/update JD text (owner + in_progress only) |
| POST | /:id/finish |
Finalize + generate fallback feedback if needed |
Voice (WebSocket)
| Path | Notes |
|---|---|
WS /api/interviews/:id/voice |
Cookie-authenticated, ownership-checked bridge to the Deepgram Voice Agent. Client streams PCM in, receives PCM + control events out. |
Misc
| Method | Path | Notes |
|---|---|---|
| GET | /api/health |
Liveness check |
| Decision | Trade-off accepted |
|---|---|
| Deepgram Voice Agent API (single WS) over building my own STTβLLMβTTS pipeline or using LiveKit | Less control over each stage, but native barge-in, one round trip instead of three, and one secret. Rolling my own means owning VAD/endpointing/buffering; LiveKit is just transport β I'd still orchestrate the three model calls myself. |
| Prisma ORM instead of raw SQL | Extra dependency + migration step, but type-safe queries and versioned migrations β safer as the schema kept evolving. |
| Client-gated mic (auto-mutes while the agent speaks; tap to interrupt) instead of always-on talk-over | Less "natural" β you tap to interrupt β but background noise can't accidentally cut off the interviewer. Calmer, more predictable. |
Raw ws instead of the Deepgram SDK socket |
More boilerplate, but the SDK v5 client corrupts binary audio frames β raw ws avoids the bug. |
| LangGraph director with a heuristic fallback | An extra LLM hop (Groq) vs. letting the model run autonomously, but gives deterministic difficulty/topic routing. Falls back to rule-based logic if the key's missing β keeps running with zero extra keys. |
| Service | Role in app | Config |
|---|---|---|
| Deepgram Voice Agent API | The whole voice loop over one WebSocket: Nova-3 STT + Deepgram-managed LLM + Aura-2 TTS | Standard tier |
Groq gpt-oss-120b |
LangGraph "director" brain: scores answers, adjusts difficulty, picks next question | Optional (heuristic fallback if no key) |
Deepgram bills on WebSocket connection time, not just speech β idle/listening time counts.
Deepgram β Voice Agent API, Standard tier (per minute)
| Plan | Price |
|---|---|
| Pay As You Go | $0.075/min |
| Growth | $0.068/min |
Groq β gpt-oss-120b (per 1M tokens)
| Input | Output |
|---|---|
| $0.15 | $0.60 |
Assumptions: ~10 min average call (soft-wrap 7 min, hard cap 11 min); ~8 answer exchanges; Groq called ~2β3Γ per exchange + final feedback β 45K input / 4K output tokens per interview.
| Component | Per interview | Share |
|---|---|---|
| Deepgram Voice Agent (Standard, PAYG) β 10 min Γ $0.075 | $0.750 | ~99% |
| Groq gpt-oss-120b β 45K Γ $0.15/M + 4K Γ $0.60/M | $0.009 | ~1% |
| Total (PAYG) | β $0.76 | |
| Total on Growth plan (10 min Γ $0.068 + Groq) | β $0.69 |
Deepgram is ~99% of cost; Groq is a rounding error (< 2Β’). Every optimization dollar is in Deepgram minutes.
| Volume | Deepgram | Groq | Total |
|---|---|---|---|
| 100 interviews | $75 | $0.90 | ~$76 |
| 1,000 interviews | $750 | $9 | ~$759 |
| 10,000 interviews | $7,500 | $90 | ~$7,590 |
Biggest lever: average call length. Trimming the hard cap 11β8 min, or landing most calls near the 7-min soft nudge, cuts Deepgram spend ~20β30% linearly.
Deepgram β per project (429 on exceed)
| API | Pay As You Go | Growth |
|---|---|---|
| Voice Agent (WSS) β ours | 45 concurrent connections | 60 (NA) / 45 (EU/AU) |
| Streaming STT (WSS) | 150 | 225 (NA) |
| TTS streaming | 45 | 60 (NA) |
Groq β free tier (current constraint on gpt-oss-120b)
| Limit | Free tier |
|---|---|
| Requests / min | 30 RPM |
| Requests / day | 1,000 RPD |
| Tokens / min | 8,000 TPM |
| Tokens / day | 200,000 TPD |