Real-time AI interview simulator. Practice technical and behavioral interviews with voice, code, and system design — with an AI that adapts to your target company, role, and preferred style.
| Area | Capabilities |
|---|---|
| Voice Interview | Real-time bidirectional audio, AI can interrupt, incremental live transcription |
| DSA Coding | LeetCode-style problems, in-browser editor (Python/C++/TS), event-driven code review |
| System Design | Excalidraw whiteboard, AI reads your diagrams, canvas suggestions via structured markers |
| Company Profiles | 16 companies with culture, interviewer behavior, and role-specific topics |
| Interview Styles | Supportive, Professional, Challenging, Bar Raiser — each with different interruption and probing patterns |
| Depth Levels | Standard, Probing, Challenge, Bar Raiser — control how many follow-ups and how hard |
| Rounds | Company-specific round selection (Phone Screen, Technical Deep Dive, System Design, Behavioral) |
| Resume & GitHub | Upload resume or link GitHub for personalized, targeted questions |
| Evaluation | Post-interview AI scoring across 6 dimensions, skill profile tracking over time |
| Queue System | Redis-backed FIFO queue when session limit is reached, real-time position updates |
ai-interview/
├── apps/
│ ├── backend/ # Elysia.js HTTP server + WebSocket server
│ │ ├── src/
│ │ │ ├── prompt.ts # Dynamic prompt assembly engine
│ │ │ ├── gemini.ts # AI session wrapper (model-agnostic interface)
│ │ │ ├── ws/
│ │ │ │ ├── index.ts # WebSocket server entry
│ │ │ │ ├── session.ts # InterviewConnection class
│ │ │ │ ├── dedup.ts # Transcription deduplication
│ │ │ │ └── finalize.ts # Post-interview finalization
│ │ │ ├── lib/
│ │ │ │ ├── redis.ts # Redis client (queue backend)
│ │ │ │ ├── queue.ts # Queue helpers (tryActivate, enqueue, dequeue)
│ │ │ │ └── email.ts # Email templates (OTP, welcome, feedback)
│ │ │ └── routes/ # Auth, user, resume, interview, feedback, pricing
│ │ └── prisma/ # PostgreSQL schema + migrations
│ └── frontend/ # React 19 SPA
│ └── src/
│ ├── pages/ # NewInterview, Interview, Results, Dashboard, Feedback, Pricing
│ ├── components/ # CompanyGrid, RolePicker, SessionControls, InterviewQueue, Landing*
│ ├── hooks/ # useMicrophone, useAudioPlayer
│ └── lib/ # WebSocket client, API client, auth
├── packages/
│ ├── shared/ # Zod schemas, shared types, company configs
│ ├── ui/ # Shared UI primitives
│ ├── eslint-config/
│ └── typescript-config/
- Real-time bidirectional audio via WebSocket + AI audio API
- AI can interrupt mid-answer when answers go off-track
- User cannot interrupt AI — mic blocked during AI speech
- Incremental transcription with character-level deduplication
Each company has structured culture, interviewer behavior, and role-specific interview data:
| Company | Style | Depth | Roles |
|---|---|---|---|
| Stripe | Challenging | Challenge | Backend, Payments, Platform |
| Amazon | Bar Raiser | Challenge | SDE, PM, Solutions Architect |
| Professional | Probing | SWE, Data Scientist, UX Engineer | |
| Meta | Challenging | Probing | Frontend, ML, Infrastructure |
| Netflix | Bar Raiser | Bar Raiser | Backend, Data, SRE |
| Microsoft | Professional | Standard | SWE, DevOps, AI Engineer |
| Apple | Challenging | Probing | iOS, Hardware, Security |
| Uber | Professional | Challenge | Backend, Mobile, Data Science |
| Airbnb | Supportive | Probing | Fullstack, Design, Staff |
| Datadog | Professional | Standard | SRE, Cloud, Support |
| Deloitte | Professional | Probing | Consultant, Data Analyst, Cloud |
| Goldman Sachs | Bar Raiser | Challenge | Quant Dev, Risk, Platform |
| Palantir | Challenging | Bar Raiser | FDE, Data, Security |
| Figma | Supportive | Standard | Design Engineer, Frontend, Platform |
| Notion | Supportive | Standard | Fullstack, Mobile, Infra |
| Startup | Supportive | Challenge | CTO, Founder, Staff |
Roles define topics, evaluationCriteria, and mustProbe — so a Stripe Backend interview (distributed systems, APIs, caching) is completely different from a Stripe PM interview (prioritization, metrics, stakeholder management) even with the same style and depth.
Controls how questions are asked:
| Style | Approach | Interruption |
|---|---|---|
| Supportive | Conversational, encouraging | Rare, gentle redirection |
| Professional | Structured, neutral | When unfocused or repetitive |
| Challenging | High-pressure, push for depth | Aggressive, cut off off-track answers |
| Bar Raiser | Elite, surgical | Strategic — highest leverage point only |
Controls how many follow-ups and how hard each question is probed:
| Depth | Follow-ups | Challenge Level |
|---|---|---|
| Standard | None | Smooth, conversational |
| Probing | 1-2 per topic | Gentle elaboration requests |
| Challenge | Until defended | Disagree, demand metrics |
| Bar Raiser | Maximum rigor | Skepticism, evidence required |
- 4 unique rounds per company (e.g., Stripe: Phone Screen, Technical Deep Dive, System Design, Leadership & Behavior)
- Custom round input for roles not listed
- Progress stepper showing step position in setup flow
Instead of a single static prompt, the system assembles a dynamic prompt from layers:
Interview Objective ← Optimize for signal, not coverage
Candidate History ← Past scores, strengths, weaknesses
Company Context ← Culture + Interviewer Approach
Role Context ← Topics + Evaluation Criteria + Must Probe
Interview Style ← How to ask
Interaction Depth ← How many follow-ups
Resume / GitHub / JD ← Personalization data
Evaluation Dimensions ← What to assess (6 dimensions)
Story Extraction ← Identify reusable stories
Interview Guidelines ← Practical rules
Impact weighting: Role ~60% (drives what gets asked), Style ~25% (how), Company ~10% (cultural emphasis), Depth ~5% (follow-up count).
- Post-interview evaluation via AI
- Per-turn scoring with feedback
- 6 dimension scores: Communication, Technical Depth, Problem Solving, Leadership, Ownership, Decision Making
- Resume-strength correlation
- Candidate skill profile updates over time
- Candidates can retake interviews after 7 days (FREE tier: 3/week)
- Redis-backed FIFO queue when concurrent session limit is reached
- MAX_CONCURRENT_SESSIONS=4 (configurable)
- Real-time position updates pushed to waiting clients
- Heartbeat (30s ping / 10s timeout) to detect stale connections
- Automatic slot release and dequeue when an interview ends
- Premium editorial feedback form with Tabler-style icons and progressive bar rating
- Categories: Bug Report, Feature Request, Performance, UX, Other
- Admin dashboard for reviewing all feedback
- Automated thank-you email via Resend
- FREE tier: 3 interviews / 7 days, 15 min cap
- PRO tier: 6 interviews / 7 days, 30 min cap (contact for upgrade)
- Max tier: Coming soon
- Custom toast notifications for rate limit errors with upgrade links
Browser Mic → PCM 16kHz → WebSocket → AI Audio API → Audio + Transcription → Browser Speaker
│ │
└── audio_stream_end ───────┘
- LeetCode-style problems sourced from company-specific question data
- 1900+ companies with real historical question frequency data
- 6-phase interview: Understanding → Brute Force → Optimization → Implementation → Testing → Review
- Event-driven code review (no auto-snapshots, AI reviews on request only)
- Hidden rubric per question guides AI evaluation
- Monaco Editor in-browser with Python/C++/TypeScript support
- 25 min fixed timer with phase tracking
- Resume upload & analysis — PDF parsing, section detection, AI-tailored questions
- GitHub integration — public repo analysis for code-specific questions
- Job description parsing — paste a JD for targeted questions
- Custom company & role — AI generates interview context on the fly
- Email verification — OTP via Resend
- Interview history — dashboard with scores, feedback, improvement tracking
- Role-based access control — FREE, PRO, ADMIN tiers with different limits
| Layer | Technology |
|---|---|
| Runtime | Bun 1.3+ |
| Backend | Elysia.js |
| Frontend | React 19, motion (animations) |
| Database | PostgreSQL + Prisma |
| AI | Multi-model (Gemini, more coming) |
| Real-time | WebSocket (ws) |
| Queuing | Redis |
| Auth | JWT + OTP |
| Resend | |
| CSS | Tailwind CSS 4 |
| Icons | react-icons, Tabler Icons |
| Monorepo | Turborepo |
- Bun 1.3+ (
curl -fsSL https://bun.sh/install | bash) - PostgreSQL running locally or remotely
- Redis running locally or remotely (for queue system)
- AI API key (Gemini or compatible)
# Install dependencies
bun install
# Copy environment files
cp apps/backend/.env.example apps/backend/.env
cp apps/frontend/.env.example apps/frontend/.env
# Set up your .env files
# apps/backend/.env requires:
# DATABASE_URL=postgresql://...
# AI_API_KEY=your_key
# JWT_SECRET=...
# RESEND_API_KEY=...
# REDIS_HOST=localhost
# WS_PORT=8080
# MAX_CONCURRENT_SESSIONS=4
#
# apps/frontend/.env requires:
# VITE_API_HOST=http://localhost:3000
# VITE_WS_HOST=localhost:8080
# Run database migrations
bun run --filter @evalio/db prisma migrate dev
# Start development servers
bun run dev| Service | URL |
|---|---|
| Frontend | http://localhost:5173 |
| API | http://localhost:3000 |
| WebSocket | ws://localhost:8080 |
| Command | Description |
|---|---|
bun run dev |
Start all apps in development mode (hot reload) |
bun run build |
Build all apps and packages |
bun run lint |
Run ESLint across all packages |
bun run check-types |
Run TypeScript type checking |
bun run format |
Format code with Prettier |
bun run --filter @evalio/backend dev |
Backend only |
bun run --filter @evalio/frontend dev |
Frontend only |
| Method | Path | Description |
|---|---|---|
| POST | /api/auth/signup |
Register with email + password |
| POST | /api/auth/verify-otp |
Verify email with OTP |
| POST | /api/auth/login |
Login, receive JWT |
| GET | /api/interviews |
List user's interviews |
| POST | /api/interviews |
Create new interview |
| GET | /api/interviews/:id |
Get interview details |
| POST | /api/resumes/upload |
Upload resume (PDF) |
| GET | /api/github/profile |
Get linked GitHub profile |
| POST | /api/companies/generate |
AI-generate custom company context |
| POST | /api/feedback/submit |
Submit feedback |
| GET | /api/feedback |
List feedbacks (admin only) |
| Direction | Type | Purpose |
|---|---|---|
| Client → | init |
Authenticate, start interview session |
| Client → | audio_chunk |
Send PCM audio data |
| Client → | audio_stream_end |
Signal end of user speech |
| Client → | end_interview |
Request closing + evaluation |
| Server → | ready |
Interview initialized, listening |
| Server → | serverContent |
AI speech (audio + transcription) |
| Server → | queued |
Session queued (position in queue) |
| Server → | position_update |
Queue position changed |
| Server → | closing_started |
Interview entering closing phase |
| Server → | feedback_ready |
Evaluation complete, navigate to results |
| Server → | time_limit |
Total interview duration |
| Server → | time_warning |
1 minute remaining |
| Server → | time_limit_reached |
Time expired, closing triggered |
Free-form personality strings caused the AI to produce similar interviews across roles at the same company. By splitting into culture (company values), interviewerBehavior (approach), and role-specific topics, evaluationCriteria, and mustProbe, the generated interview varies meaningfully by role. Impact: Role ~60%, Style ~25%, Company ~10%, Depth ~5%.
In standard interview modes, each Q&A creates a new database turn. In Challenge and Bar Raiser modes, multiple user answers accumulate into a single turn until the AI asks a new question (detected via isNewQuestion() heuristic). This gives the AI room to challenge, probe, and redirect without creating spurious turn boundaries.
Interruption is detected on the frontend: when AI sends audio while the user is speaking, the mic is stopped immediately and audio_stream_end is sent. No backend round-trip needed, keeping latency low.
Instead of Kafka or PostgreSQL-based queuing, the system uses Redis Sorted Sets for FIFO queuing and Redis Sets for tracking active sessions. This provides O(log N) queue operations and real-time position updates without the overhead of a full message broker.
DSA rounds use event-driven code snapshots instead of auto-snapshots every 10 seconds. Code is sent only when the user requests review, clicks Run, says "I'm done," or the AI asks to see the current implementation — reducing storage and token cost by ~95%.
Instead of one monolithic system prompt, the prompt is assembled from independent sections (Objective, Company Context, Role Context, Style, Depth, Resume, Guidelines). Each section is independently maintainable and conditionally included. This makes it easy to tweak individual behaviors without affecting the rest.
All benchmarks run on Apple Silicon M3, 16GB RAM, macOS, against PostgreSQL 16 on localhost with the backend on port 3000.
Run with EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) via PostgreSQL 16.
| Query | Planning | Execution | Total | Scan | Buffers |
|---|---|---|---|---|---|
| Interview list (userId + createdAt sort, LIMIT 21) | 4.01 ms | 1.11 ms | 5.13 ms | Index Scan | 8 hit |
| Rate-limit count (7-day window per user) | 0.60 ms | 0.34 ms | 0.93 ms | Index Scan | 2 hit |
| Score trend (last 5 completed, scored) | 0.28 ms | 0.01 ms | 0.29 ms | Index Scan | 2 hit |
| Interview detail (with summary + resume joins) | 1.13 ms | 0.04 ms | 1.16 ms | Index Scan | 2 hit |
| Refresh token lookup (by hash) | 0.99 ms | 0.02 ms | 1.01 ms | Index Scan | 1 hit |
| Composite average | — | — | 1.70 ms | — | — |
Indexes installed:
InterviewSession(userId, createdAt)— covers list + rate-limit queriesInterviewSession(userId, status, createdAt)— covers score-trend queriesRefreshToken(tokenHash)— unique index for O(1) token rotation lookups
Estimated improvement vs unindexed (10k rows): ~29–117× faster (sequential scan ~50–200ms → index-only scan ~0.1–2ms)
Tested with fetch() under sequential and concurrent load. Auth endpoint returns 401 (expected — no token), counted as success since latency and throughput are the measure.
| Endpoint | Avg | P50 | P95 | P99 | Success |
|---|---|---|---|---|---|
GET /health |
0.2 ms | 0.1 ms | 0.7 ms | 0.7 ms | 100% |
GET /ready |
0.1 ms | 0.1 ms | 0.3 ms | 0.3 ms | 100% |
GET /api/auth/me |
0.2 ms | 0.2 ms | 0.5 ms | 0.5 ms | 100%* |
*Returns 401 as expected (no auth token) — middleware rejects fast without DB hit.
| Concurrency | Requests | Errors | Duration | Throughput | Avg | P95 | Error% |
|---|---|---|---|---|---|---|---|
| 10× | 50 | 0 | 3 ms | 19,190 req/s | 0.4 ms | 1.2 ms | 0% |
| 25× | 100 | 0 | 5 ms | 19,355 req/s | 0.9 ms | 1.9 ms | 0% |
| 50× | 200 | 0 | 9 ms | 23,318 req/s | 1.4 ms | 5.0 ms | 0% |
| 100× | 500 | 0 | 31 ms | 16,262 req/s | 3.9 ms | 6.6 ms | 0% |
Max throughput: ~23,318 req/s at 50 concurrent connections.
No errors at any load level. Latency grows linearly with concurrency.
- 3-tier system: global (100/60s per IP), strict (10/60s), auth (5/60s)
- Backend: Redis-backed sliding window
- Verified: /api/auth/me returns 401 <0.5ms — rate limiting does not add material latency
| Property | Value |
|---|---|
| Failure threshold | 3 consecutive failures |
| Half-open recovery window | 15,000 ms (configurable) |
| Auto-recovery | ✓ after window elapses |
| Request blocking during open circuit | ✓ 5/5 blocked |
| Probe on half-open | ✓ single request allowed through |
| Full reset on success | ✓ |
Production impact:
- Without: Redis outage → every request times out → cascading failure → users see 500s
- With: 3 failures → circuit opens → cache skipped for 15s → backend serves stale/DB data → 100% API availability maintained
| Mechanism | Trigger | Fallback | Recovery | Verified | Lines |
|---|---|---|---|---|---|
| Cache circuit breaker | 3× Redis failure | DB fallback (null return) | 15s half-open probe | try/catch ✓ | 140 |
| Queue bypass | Redis connection error | No-op state (continue) | Next op auto-retry | try/catch ✓ | 110 |
| Email buffer | Resend API non-2xx | PendingEmail PostgreSQL insert |
Exp. backoff 5s→25s→125s (3 attempts) | try/catch ✓ | 508 |
Email retry backoff schedule:
- Attempt 1: wait 5s
- Attempt 2: wait 25s (cumulative 30s)
- Attempt 3: wait 125s (cumulative 155s)
Without degradation:
- Redis outage → 100% cache failure → 500 errors
- Email outage → emails lost permanently
- Queue outage → interview creation blocked
With degradation:
- Cache → direct DB (100% uptime)
- Email → DB buffer → retry (100% delivery rate)
- Queue → bypass (100% uptime)
| Metric | Value | Source |
|---|---|---|
| Avg DB query time | 1.70 ms | EXPLAIN ANALYZE, 5 queries |
| Avg API response time | 0.2 ms | 60 sequential requests |
| Peak throughput | 23,318 req/s | 50 concurrent connections |
| P95 latency at 100× concurrency | 6.6 ms | 500 concurrent requests |
| Circuit breaker trip | 3 failures | Verified |
| Circuit breaker recovery | 15 s (half-open window) | Verified |
| Email delivery during outage | 100% | PendingEmail DB buffer |
| API availability during Redis outage | 100% | Graceful degradation path |
| Rate limiting tiers | 3 (global/strict/auth) | Redis-backed |
| Account lockout | 5 failures / 15 min | Redis-backed |
# Prerequisites
# - PostgreSQL on localhost:5432
# - Backend on :3000 (bun run dev)
# Run all benchmarks at once
DATABASE_URL="postgresql://postgres:mysecretpassword@localhost:5432/postgres" \
PORT=3000 \
bash benchmarks/run-all.sh
# Or run individually
bun run benchmarks/01-db-queries.ts
bun run benchmarks/02-api-peak-load.ts
bun run benchmarks/03-circuit-breaker.ts
bun run benchmarks/04-graceful-degradation.tsMIT
