Case study 01

An AI interviewer for job seekers in the Philippines.

Role
Solo founder engineer
Year
2026, in development
Stack
Next 16, React 19, Supabase, Drizzle, Hono, Anthropic SDK
Links
1,064commits on main1
6workspace packages: ai, core, db, evals, library, ui2
19versioned prompts, each needing a recorded eval outcome3
CIa gate fails any prompt change that has no eval result4
  1. git rev-list --count origin/main in the ai-interviewer repo
  2. packages/*/package.json on origin/main
  3. packages/ai/src/prompts/*.v1.ts on origin/main
  4. scripts/check-prompt-evals.ts, run by pnpm verify in .github/workflows/ci.yml

The problem

Interview practice is usually a friend who is too kind or a coach who costs too much. A job seeker needs honest, specific feedback tied to the role they are actually applying for, and a place to practise saying the answers out loud.

Kaivi is a product for that gap, aimed at job seekers in the Philippines. I am building it alone, so every decision below had to be cheap to run and cheap to change.

What I built

A mobile-first web app with a separate voice worker. You upload a resume and a job description. Kaivi plans the interview from both, runs it turn by turn by voice or text, and finishes with a report on what to fix.

  • Brief: reads the resume and job description and plans the interview.
  • Drill coach: asks, follows up and adapts to the level you picked.
  • Evidence: every point in the report cites what the candidate actually said.

The monorepo has the Next 16 web app and a Hono voice worker, plus packages for the AI layer, core logic, the Drizzle database layer, the role library, shared UI and the evals.

Architecture

Web appNext 16, React 19Voice workerHonopackages/aibrief, coach, evidenceClaude APIAnthropic SDKSupabaseauth, Postgresevals in CI

AI design

  • Models by role: the report, the live interviewer and the cheap utility calls use different Claude models, chosen by cost per interview and recorded in ADRs 0011 and 0019.
  • Prompt caching: stable prefix first, volatile content last, so a long interview does not pay for its prefix again on every turn.
  • Untrusted input: resumes and answers go into delimited blocks, with any tag that could close or fake a block broken first. They never enter the system prompt.
  • Usage and pricing: token use is metered per call, priced from a table in packages/ai/src/pricing.ts, and unit tested.
  • Evals: a dedicated evals package holds a golden set of briefs and a judge. A repo check fails when a prompt file changes without a recorded eval outcome.
  • Fakes for everything: every model call has a deterministic fake, so the unit suite never touches the network.

How it was built

With a Claude Code workflow I designed. A /feature command chains brainstorming, a written plan, parallel implementers on separate files, a fresh-context reviewer and a verify step. Hooks block destructive commands and run the verify script when a session stops.

The decisions live in 22 numbered ADRs, each with the evidence it was based on, so the reason for a choice outlasts my memory of it.

Tradeoffs

  • Turn-based voice, not live conversation. ADR 0010 dropped a streaming voice cascade after the cost model showed it missing the per-interview cap. You tap to answer, audio uploads in chunks, and the next line is generated while you wait. A hands-free conversation mode is deferred. Speech to text, the live model and text to speech sit behind interfaces, so adding it later is a transport swap.
  • One eval is still a template. The Sonnet versus Opus comparison for the report (docs/evals/2026-11-report-sonnet-vs-opus.md) records Outcome: pending. The gate exists and blocks merges, but that specific number is not in yet.
  • Auth hardening is deferred. Google sign-in, MFA for admin and account deletion are listed in ADR 0004 as a later spec.
WorkHire me