A Next.js app that turns behavioral-interview prep into a card game. Draw a random question card, answer out loud, and Claude plays the interviewer: it analyzes each answer and asks realistic follow-ups. After 5 main questions it delivers a scored feedback report β what was strong, what to improve, and what the interviewer expected. Everything (sessions, questions, full conversation logs, feedback) is persisted to SQLite.
| Landing | Interview |
|---|---|
![]() |
![]() |
- π€ Speech-to-text β answer by talking (Web Speech API)
- π Text-to-speech β the interviewer reads questions aloud
- π§ Adaptive follow-ups β generated from your actual answer (
claude-opus-4-8) - π§© Pick your answer framework β STAR, PAR, or CARL; the interviewer and feedback adapt to it
- π― Pick your target SWE level β Google L3βL7; questions and the grading bar calibrate to it
- π Persistent record β every turn + feedback saved to SQLite
At the start of a session you choose how you want to structure your answers. The choice is
stored on the session and rewrites the interviewer's system prompt and the final feedback
rubric (src/lib/methodologies.ts).
| Framework | Components | Best for |
|---|---|---|
| STAR | Situation Β· Task Β· Action Β· Result | The default β most behavioral questions. |
| PAR | Problem Β· Action Β· Result | Concise, punchy answers. |
| CARL | Context Β· Action Β· Result Β· Learning | Growth / failure / learning questions (probes reflection). |
The interviewer probes for whatever component is missing or vague β e.g. in CARL it will specifically ask what you learned if you describe an outcome but never reflect on it. The feedback report then scores your answers against that same framework.
You also choose a target Google SWE level at the start. Where the framework sets the
structure of an answer, the level sets the bar it's judged against
(src/lib/levels.ts). The level affects two things:
- Question selection β cards are tagged with a level band; the deck is drawn to suit your target (junior cards for L3, scope/strategy cards for L6βL7), widening gracefully if a band is sparse.
- Grading bar β the interviewer's probing and the final feedback are calibrated to that level's behavioral expectations (scope, ambiguity, ownership, influence). The 1β10 rating is anchored to the chosen level, so a 7/10 at L6 β a 7/10 at L3.
| Level | Title | Bar (behavioral) |
|---|---|---|
| L3 | SWE II | Reliable execution & ownership of well-scoped tasks. |
| L4 (default) | SWE III | Independent feature/component ownership. |
| L5 | Senior | Team-level ownership; influence without authority. |
| L6 | Staff | Sustained multi-team / org-radius direction-setting. |
| L7 | Senior Staff | Multi-org technical direction, as a sustained pattern. |
The level bars are grounded in researched, cross-checked Google leveling rubrics. Example: answer an L6 session with a single-team story and the interviewer will push you on the cross-team / org-level radius β the L6 distinctive signal.
- Node.js β₯ 18.18 (Next.js 15 requirement).
.nvmrcpins 22.21.1 β runnvm use.better-sqlite3is a native addon compiled for the Node it was installed under, so use one consistent Node version (see Troubleshooting). - An Anthropic API key (console.anthropic.com). Without a key the app still runs end-to-end using built-in fallback follow-ups/feedback.
- Voice features work best in Chrome / Edge (Web Speech API). You can always type instead.
nvm use # Node 22.21.1 (from .nvmrc) β important
npm install
cp .env.local.example .env.local # then paste your ANTHROPIC_API_KEY
npm run db:seed # optional β DB also auto-seeds on first run
npm run dev # http://localhost:3000dev / start / db:seed run a Node-version preflight (scripts/check-node.mjs)
that stops with a clear message if Node is too old or the native binary doesn't match.
Landing ββPOST /api/sessionβββΆ pick 5 random cards, create session, log intro + first card
β
Interview page (client)
β speak/type answer
ββ POST /api/answer { sessionId, content }
β ββ log answer ββΆ Claude analyzes ββΆ { followup | next }
β followup β log follow-up, stay on card (max 2)
β next β advance; log next card, or finish
β
ββ when 5 cards done β POST /api/feedback
ββ Claude reads whole transcript ββΆ scored feedback ββΆ saved + shown
The interviewer prompt, follow-up cap, and feedback prompt live in
src/lib/anthropic.ts. The model returns structured JSON
(output_config.format) with adaptive thinking, so decisions are reliable to parse.
File: data/qcard.db (auto-created; WAL mode). Schema in
src/lib/db.ts:
| Table | Purpose |
|---|---|
questions |
The card deck (category, text, difficulty, level_min/level_max band). Seeded from questions.ts. |
sessions |
One interview: status, target main-question count, current index, chosen framework (methodology), target level (level), timestamps. |
session_questions |
The 5 cards chosen for a session, their order, status, and follow-ups asked. |
messages |
Full conversation log β every interviewer line, candidate answer, and follow-up, tagged by role + kind and linked to its card. |
feedbacks |
Final report per session: strengths, improvements, expectations, overall, rating. |
sessions 1ββββ session_questions 1ββββ messages
sessions 1ββββ messages (intro / feedback rows have null question)
sessions 1ββββ feedbacks
questions 1ββββ session_questions
The interviewer is backed by a pluggable provider layer (src/lib/llm/).
Pick one with QCARD_PROVIDER:
| Provider | QCARD_PROVIDER |
Key | Default model | Key var |
|---|---|---|---|---|
| Anthropic Claude | anthropic (default) |
console.anthropic.com | claude-opus-4-8 |
QCARD_ANTHROPIC_MODEL |
| Google Gemini | gemini |
aistudio.google.com/apikey | gemini-2.5-flash |
QCARD_GEMINI_MODEL |
Both use schema-constrained JSON output so decisions/feedback parse reliably
(Anthropic output_config.format + adaptive thinking; Gemini responseSchema,
thinking off on flash tiers). The active provider is shown as a chip in the UI.
If the selected provider has no key, the app falls back to built-in responses.
Switch example β in .env.local:
QCARD_PROVIDER=gemini
GEMINI_API_KEY=...
# QCARD_GEMINI_MODEL=gemini-2.5-proAdd another provider by implementing the JsonLLM interface in
src/lib/llm/types.ts and registering it in
src/lib/llm/index.ts.
| Variable | Default | Meaning |
|---|---|---|
QCARD_PROVIDER |
anthropic |
LLM backend: anthropic or gemini. |
ANTHROPIC_API_KEY |
β | Key for the Anthropic provider. |
QCARD_ANTHROPIC_MODEL |
claude-opus-4-8 |
Claude model (also accepts legacy QCARD_MODEL). |
GEMINI_API_KEY |
β | Key for the Gemini provider (GOOGLE_API_KEY also read). |
QCARD_GEMINI_MODEL |
gemini-2.5-flash |
Gemini model. |
QCARD_MAIN_QUESTIONS |
5 |
Main questions per interview. |
QCARD_MAX_FOLLOWUPS |
2 |
Max follow-ups the interviewer may ask per card. |
QCARD_DB_PATH |
<projectRoot>/data/qcard.db |
SQLite file location. If the configured path doesn't exist, the app searches the project structure (anchored on package.json, from both cwd and the module dir) for an existing qcard.db before creating a new one β so it resolves correctly even when launched from another directory. |
src/
app/
page.tsx landing / start
interview/[id]/page.tsx interview screen
api/
session/route.ts POST create session
session/[id]/route.ts GET load state
answer/route.ts POST answer β analyze β follow-up / next
feedback/route.ts POST generate + save feedback
components/
InterviewClient.tsx chat UI, mic, TTS, progress
FeedbackReport.tsx final report
hooks/useSpeech.ts Web Speech API (STT + TTS)
lib/
db.ts SQLite schema + queries
llm/
index.ts provider selection + analyze/feedback ops
prompts.ts shared prompts, schemas, fallbacks
anthropic.ts Anthropic (Claude) provider
gemini.ts Google Gemini provider
types.ts JsonLLM provider interface
methodologies.ts STAR / PAR / CARL frameworks + prompt guidance
levels.ts Google L3βL7 bars + interviewer/feedback calibration
questions.ts card bank (level-banded)
state.ts build client state from DB
types.ts shared types
The interviewer voice uses the browser's built-in voices, which vary a lot by browser/OS. QCard auto-selects the most natural English voice available and offers a picker in the UI.
- Best, no setup: use Chrome or Edge β they ship neural "Google US English" / "Natural" voices.
- Safari / macOS: defaults to Samantha. For human-quality voices, download them once: System Settings β Accessibility β Spoken Content β System Voice β Manage Voices β English β pick a Siri, (Enhanced), or (Premium) voice. They then appear in the dropdown and are auto-preferred. (macOS also ships novelty voices like Zarvox / Bad News β QCard filters those out so they're never auto-selected.)
ERR_DLOPEN_FAILED / "compiled against a different Node.js version" on Start Interview
β better-sqlite3's native binary is ABI-locked to the Node it was built under. You
installed under one Node and are running under another (common: nvm default is older
than the project Node). Fix:
nvm use # switch to .nvmrc (22.21.1), then re-run
# or, to use a different Node, rebuild the addon for it:
npm rebuild better-sqlite3The predev/prestart guard now catches this before the server starts.
- API routes run on the Node.js runtime (
better-sqlite3is native);next.config.mjsmarks it as a server-external package. - The DB is created on first request β no manual migration step needed.
MIT β see LICENSE.

