Skip to content

About

An AI-powered interview coach and job finder. It generates tailored mock interviews from your resume and target job descriptions with rubric-based scoring, and matches your skills with real job listings.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

MEMORANDUM
──────────────────────────────────────────────────────────────────────────────
TO:       Candidates preparing for their next interview loop
FROM:     InterviewIQ
RE:       Interview readiness: deliverables and commitments
──────────────────────────────────────────────────────────────────────────────
  ✔  Intake          resume + job description parsed
  ✔  Agenda          8 questions generated, grounded in your experience
  ✔  Review rubric   clarity · specificity · result orientation · relevance
  ✔  Scoring         computed by the backend, never by the model
  ✔  Placement       real listings, scored 0–100 against your resume
  ✖  Fabrication     none. Failures are disclosed, and you can retry.

InterviewIQ turns a resume and a target job description into a tailored mock interview. It writes eight grounded questions and scores each answer on a transparent four-part rubric. Then it shows a stronger example answer and tracks your progress over time. The AI Job Finder finds real listings that fit your resume and turns any of them into an interview in one click.

8 questions per session · 4 rubric dimensions · 0–100 job match · 128 backend tests · 0 fabricated scores

01  ·  Product Portfolio

■ Tailored sessions

Resume + JD analysis, then exactly 8 questions: 2 technical, 2 project deep-dive, 2 behavioral, 1 system design or case study, 1 motivation. Every question is schema validated, with a bounded repair-retry loop.

■ Rubric evaluation

Four integer scores (1–5), each with a reason, plus overall feedback and the one thing to fix. The percentage is avg / 5 × 100, computed by the backend and never taken from the model.

■ Interview workspace

One question at a time, answered in order. Drafts autosave locally. Progress is restored from the database on refresh, and duplicate submits and multiple open tabs are handled safely.

■ Stronger answer

A STAR-shaped example (behavioral questions only), What changed, missing details shown as [placeholders], and a likely follow-up. It's saved and reused unless you regenerate it.

■ AI Job Finder

Upload a PDF/DOCX resume, set roles, location and preferences, and get real listings with a 0–100 match score. Then Prepare for interview on any of them.

■ Dashboard + history

Real aggregates, a score-trend chart (with a table view) and rubric averages. History has search, role filter, sort, pagination, and delete with confirmation.

■ Further offerings: auth, failure handling, settings, demo mode
  • Auth: Supabase email/password sign-up, login, logout, password reset and protected routes. The backend verifies every JWT, via JWKS signing keys or the legacy HS256 secret.
  • Failure handling: an answer is stored before evaluation. A failed evaluation is marked failed and offers Retry, and scores are never fabricated.
  • Session review: expand or collapse each question, compare your answer with the example, keep the resume hidden by default, and Practice again.
  • Settings: display name, theme (light/dark/system), AI provider disclosure, sign out, and account deletion.
  • Demo mode: passwordless, isolated demo accounts with labelled sample history, plus an offline heuristic "AI" that's always labelled as not an AI model.

■ Product walkthrough

Landing page Dashboard with score trend
/ landing /dashboard progress and rubric averages
Interview workspace Answer evaluation
/interviews/:id one question at a time four-dimension evaluation
AI Job Finder
/jobs AI Job Finder

Note

These screenshots were captured in demo mode: the sample history is fictional and the scores come from the offline heuristic, which the UI labels as such.

02  ·  Talent Acquisition: AI Job Finder

/jobs (sidebar: Job finder). Upload a resume (PDF/DOCX, or paste / reuse your latest). Set target roles, location, level, work arrangement and employment type, and see real listings scored against your resume. Prepare for interview sends a listing (plus the full description you paste, if the listing only has a summary) into the interview engine.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#0b1f3a','primaryTextColor':'#ffffff','primaryBorderColor':'#c9a227','lineColor':'#c9a227','secondaryColor':'#2a4a75','tertiaryColor':'#f4f6f9','textColor':'#0b1f3a','edgeLabelBackground':'#f4f6f9','clusterBkg':'#f4f6f9','clusterBorder':'#8a9ab0','titleColor':'#0b1f3a','fontFamily':'Helvetica, Arial, sans-serif'}}}%%
flowchart LR
    R[/"Resume"/] -- "contacts redacted" --> P["01 · Screening<br>skills-only profile"]
    P -- "keywords + location" --> S["02 · Sourcing<br>Adzuna listings"]
    S --> M["03 · Assessment<br>score 0–100"]
    M --> I(["Interview prep<br>8-question session"])
Loading
Stage What runs Data sent out
01 Analyse resume Gemini extracts a skills-only profile Resume with contact details removed
02 Discover listings Adzuna job-search API (ADZUNA_*) Role keywords + location only, never the resume
03 Evaluate matches One batched Gemini call; score computed in code Profile + redacted resume + listing text
■ How the 0–100 score is computed
score = 100 × ( 0.40 · skill_coverage      ← computed from evidence in your resume
              + 0.25 · experience          ┐
              + 0.20 · role alignment      ├ AI ratings 1–5, mapped to 0–1
              + 0.15 · responsibilities )  ┘
  • If a listing names no skills, the skills weight is dropped and the rest renormalised.
  • Labels: ≥75 Strong · ≥55 Good · ≥35 Partial · else Weak.
  • It estimates resume-to-listing alignment, not hiring probability.
  • It is labelled provisional unless scored against a full description you provided, because Adzuna returns summaries only.
■ Why Adzuna instead of Gemini Google Search grounding

The Gemini API terms for grounding forbid analysing, caching, altering or mixing other content into grounded results, and a job finder must do all of those. Adzuna is a licensed job-search API with a free developer tier.

  • Its terms require an "Adzuna" attribution on each listing, which every card renders.
  • Organisations may use it on trial for 14 days, and commercial use "may require a licence agreement". Check this with Adzuna before a commercial launch.
  • Default limits: 25/min, 250/day, 2,500/month.
■ Honesty rules and storage
  • Only listings returned by the provider are shown, and none are ever generated.
  • Links are restricted to the provider's own HTTPS listing URLs.
  • Missing fields show "Not listed", and predicted salaries are hidden.
  • Listings are never claimed to be open.
  • Storage:
    • job_searches holds preferences, queries and the skills-only profile.
    • job_results holds listing fields, match analysis and the linked interview.
    • Resume files are processed in memory and never stored, and resume text stays in the browser tab.
    • Both tables are deleted with the account.

03  ·  Organisational Structure

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#0b1f3a','primaryTextColor':'#ffffff','primaryBorderColor':'#c9a227','lineColor':'#c9a227','secondaryColor':'#2a4a75','tertiaryColor':'#f4f6f9','textColor':'#0b1f3a','edgeLabelBackground':'#f4f6f9','clusterBkg':'#f4f6f9','clusterBorder':'#8a9ab0','titleColor':'#0b1f3a','fontFamily':'Helvetica, Arial, sans-serif'}}}%%
flowchart LR
    subgraph Browser
        W["React + Vite SPA<br>Render static site"]
    end
    subgraph Render
        A["FastAPI<br>Docker web service"]
    end
    subgraph Supabase
        AU["Auth<br>login only"]
        DB[("Postgres<br>+ RLS")]
    end
    AI["Gemini / OpenAI<br>server-side keys"]
    JOBS["Adzuna<br>job-search API"]
    W -- "HTTPS + JWT" --> A
    W -. "sign in" .-> AU
    A -- "verify JWT (JWKS)" --> AU
    A --> DB
    A --> AI
    A --> JOBS
Loading
Layer Choice Where
Frontend React 18, TypeScript, Vite, Tailwind, shadcn-style components (Radix), TanStack Query, RHF + Zod, Recharts, Lucide frontend/
Backend Python 3.12, FastAPI, Pydantic v2, SQLAlchemy 2 (async) backend/app
DB Supabase Postgres + RLS (prod); SQLite (local demo + tests) supabase/migrations
Auth Supabase Auth; backend verifies JWT (JWKS or HS256) app/core/security.py
AI Provider abstraction: gemini, openai, demo, with model fallback app/ai/
Jobs Provider interface + Adzuna adapter, normalisation, deterministic scoring app/jobs/
■ Backend source map
backend/app
├── main.py                 app factory: config validation, middleware, CORS, routers
├── asgi.py                 uvicorn entry point
├── core/                   config, security (JWT), logging (JSON + request IDs), exceptions, rate limit
├── api/routes/             health, sessions, answers, analytics, account, jobs (+ demo)
├── schemas/                public API request/response models
├── models/                 SQLAlchemy ORM (mirrors the SQL migrations)
├── repositories/           all queries, every one scoped by user_id
├── services/               interview, question, evaluation, rewrite, analytics, account, demo, job finder
├── jobs/                   resume extraction, job providers, normalisation, match scoring
└── ai/
    ├── provider.py         Gemini / OpenAI over httpx; error mapping; model fallback; cost estimates
    ├── demo_provider.py    offline heuristic (explicitly selected, never a fallback)
    ├── structured.py       bounded retries + JSON repair + schema validation
    ├── schemas.py          AI output schemas (8 questions, 1–5 scores…)
    └── prompts/            version-controlled prompts
■ Key design decisions
  • The backend owns all data access.
    • The browser uses Supabase only for authentication.
    • The backend scopes every query by the verified user id.
    • It returns 404 rather than 403, so it never reveals whether a record exists.
    • RLS policies add defence in depth.
  • AI calls happen before writes. Session creation runs both AI steps first, then inserts the session and its questions in one transaction, so a failure leaves no partial rows.
  • Idempotency:
    • A UNIQUE(question_id) constraint allows one answer per question.
    • An identical resubmit returns the stored answer.
    • An atomic UPDATE … WHERE status IN (…) claim stops two tabs from evaluating, and billing, the same answer.
    • A claim stuck in evaluating is reclaimed after 4 minutes.
  • Prompt-injection hardening:
    • User text travels only as JSON inside <untrusted_data>, with < and > escaped so it can't close the block.
    • System prompts tell the model to treat that data as data, never as instructions.
    • All model output is schema-validated.
    • The LLM has no tools, SQL or credentials.
  • Data minimisation:
    • Email addresses, phone numbers and links are redacted before AI calls.
    • Logs contain IDs, sizes and durations, never resume text, answers or tokens.

04  ·  Onboarding

Tip

No credentials needed. Demo mode runs on SQLite with an offline heuristic in place of the AI. Prerequisites: Python 3.12+ and Node 20+.

■ Windows: one command
powershell -ExecutionPolicy Bypass -File .\start-dev.ps1
■ Manual: macOS / Linux / Windows
# Backend (demo mode, SQLite, offline demo AI)
cd backend
python -m venv .venv && source .venv/bin/activate      # Windows: .venv\Scripts\activate
pip install -r requirements-dev.txt
cp .env.example .env                                     # defaults: DEMO_MODE=true, AI_PROVIDER=demo
uvicorn app.asgi:app --reload --port 8000

# Frontend (new terminal)
cd frontend
npm install
cp .env.example .env.local                               # VITE_API_BASE_URL=http://localhost:8000
npm run dev

Open http://localhost:5173, choose Log in, then Explore the demo. API docs are at http://localhost:8000/docs.

Warning

Seeing {"error":{"code":"not_found",...}}? Port 8000 is the API, not the website. Open http://localhost:5173 instead (the web app must be running).

To get real AI feedback while still using demo login, set AI_PROVIDER=gemini and GEMINI_API_KEY=… (or openai / OPENAI_API_KEY) in backend/.env and restart.

■ Full local setup: Supabase + real AI
  1. Create a Supabase project at supabase.com.
  2. Apply migrations once DATABASE_URL is set in backend/.env (step 5):
    • Run cd backend && python -m scripts.apply_migrations. It is safe to re-run.
    • Or paste supabase/migrations/*.sql into the SQL editor in order, or use supabase db push.
    • Then run python -m scripts.check_setup to verify the database, auth keys and AI key without printing secrets.
  3. (Recommended) Run the RLS isolation test on a non-production project: psql "$DB_URL" -f supabase/tests/rls_isolation_test.sql. It runs inside a rolled-back transaction.
  4. Configure Auth. Under Authentication → URL Configuration, set the Site URL to http://localhost:5173 and add http://localhost:5173/reset-password as a redirect URL.
  5. backend/.env:
    DEMO_MODE=false
    DATABASE_URL=postgresql://postgres.<ref>:<password>@aws-0-<region>.pooler.supabase.com:5432/postgres
    DB_AUTO_CREATE=false
    SUPABASE_URL=https://<ref>.supabase.co
    SUPABASE_JWT_SECRET=            # only if your project still uses legacy HS256 JWT secret
    SUPABASE_SERVICE_ROLE_KEY=...   # optional: lets account deletion remove the auth user
    AI_PROVIDER=gemini
    GEMINI_API_KEY=...
    ADZUNA_APP_ID=...               # optional: enables live job search (developer.adzuna.com)
    ADZUNA_APP_KEY=...
  6. frontend/.env.local:
    VITE_API_BASE_URL=http://localhost:8000
    VITE_SUPABASE_URL=https://<ref>.supabase.co
    VITE_SUPABASE_ANON_KEY=<public anon / publishable key>
■ Environment variables
Variable Where Required Purpose
APP_ENV backend – development / test / production (stricter startup checks)
FRONTEND_ORIGIN backend prod Comma-separated CORS allowlist
DATABASE_URL backend prod Postgres URL (Supabase session pooler recommended)
DB_AUTO_CREATE backend – true only for SQLite/dev; production uses migrations
SUPABASE_URL backend prod JWKS verification + admin user deletion
SUPABASE_JWT_SECRET backend legacy HS256 verification for legacy projects
SUPABASE_SERVICE_ROLE_KEY backend optional Deletes the auth user on account deletion
DEMO_MODE, DEMO_JWT_SECRET, DEMO_SESSIONS_PER_HOUR backend – Demo accounts (SQLite only)
AI_PROVIDER backend ✔ gemini / openai / demo
GEMINI_API_KEY / OPENAI_API_KEY backend per provider Never exposed to the browser
AI_MODEL, AI_FALLBACK_MODELS backend – Primary model, then comma-separated models tried in order when it is overloaded (503/429/timeout)
AI_TIMEOUT_SECONDS, AI_MAX_RETRIES, AI_RETRY_BACKOFF_SECONDS backend – Bounded retry policy
RATE_LIMIT_PER_MINUTE backend – Per-user AI request limit
ADZUNA_APP_ID, ADZUNA_APP_KEY, ADZUNA_COUNTRY backend for job finder Licensed job-search provider
JOB_SEARCH_*, JOB_SEARCHES_PER_HOUR, RESUME_UPLOAD_MAX_MB backend – Job finder cache, limits and upload size
VITE_API_BASE_URL frontend ✔ Backend URL
VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY frontend for real auth Public values only

The backend refuses to start with an inconsistent configuration. For example, it rejects a real provider without a key, production with SQLite or localhost CORS, and demo mode with Postgres.

05  ·  Go-to-Market

In production now:

Layer Host URL Source
Web app Vercel interviewiq-pink.vercel.app frontend/ (vercel.json)
API Render (Docker) interviewiq-api-98cw.onrender.com/health backend/ (render.yaml)
Database + Auth Supabase Postgres with RLS, Supabase Auth supabase/migrations

Every push to main redeploys both: Vercel builds the web app from frontend/, and Render rebuilds the API image.

To deploy your own copy:

  1. API on Render: dashboard → New → Blueprint → select this repo, then fill in the secrets it asks for (DATABASE_URL, Supabase keys, GEMINI_API_KEY, Adzuna keys). The Blueprint also defines an optional interviewiq-web static site; skip it if you host the frontend on Vercel.
  2. Web app on Vercel: import the repo, set Root Directory to frontend, and add the three public values VITE_API_BASE_URL (the Render API URL), VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY.
  3. Connect them: set FRONTEND_ORIGIN on the API to the Vercel URL (comma-separate several origins). The API refuses to start in production with a localhost origin.
  4. Supabase Auth: under Authentication → URL Configuration, set the Site URL to the web app and add its /login and /reset-password as redirect URLs.

Important

Never put DATABASE_URL, the service-role key or AI keys in a VITE_ variable: those are bundled into the browser. Render's free plan sleeps when idle, so the first request after a pause is slow. More detail is in docs/DEPLOYMENT.md.

06  ·  Audit & Compliance

cd backend
pytest                       # 128 tests, mocked AI, SQLite: no credentials needed
TEST_DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/iq_test pytest   # same suite on Postgres
pytest -m live_ai            # optional: real provider round-trip (needs a key in .env)
ruff check . && ruff format --check . && mypy app

cd frontend
npm run lint && npm run typecheck && npm test && npm run build

CI (ci.yml) runs all of this on SQLite and Postgres 16. It also applies the SQL migrations, runs the RLS isolation test, builds the frontend and builds the Docker image.

■ What the backend tests cover
  • Request validation, question-schema and count validation, invalid categories and distribution.
  • Score range and percentage maths, malformed-JSON repair, bounded retries, and transient vs permanent vs quota errors.
  • IDOR and ownership checks for every endpoint, plus expired, forged and alg=none tokens.
  • Idempotent duplicate submits, out-of-order answers, evaluation failure followed by retry, and refresh restore.
  • Rewrite caching and regeneration, analytics aggregation, rate limits, and prompt-injection isolation.
  • Job finder:
    • PDF/DOCX extraction and rejection, and the Adzuna adapter against a mocked API.
    • URL safety, de-duplication, scoring, caching, partial results and per-user isolation.
    • Prepare-interview.
  • Log hygiene, atomic rollback on DB failure, account deletion, and the evaluator metrics.
■ Demo mode
  • DEMO_MODE=true adds Explore the demo to the login page. Each visitor gets an isolated account (random UUID, 12-hour token) seeded with three fictional sessions, badged Demo in the UI.
  • AI_PROVIDER=demo replaces the AI with a deterministic keyword/statistics heuristic. A banner, a badge on every score and the [Demo heuristic] prefix in feedback all say it is not an AI model.
  • Demo mode needs the SQLite database, because demo users aren't Supabase Auth users. See DEPLOYMENT.md for a separate public demo backend.
  • With a real provider configured, the app never falls back to demo output when the provider fails. It shows an error and keeps the answer for a retry.
■ Evaluator quality

backend/evals/ contains 30 synthetic answers (10 questions × weak/medium/strong) and an agreement runner. The runner reports MAE, exact agreement, within-one agreement, quadratic weighted kappa, bias and the largest disagreements. Human labels are intentionally empty, and no agreement experiment has been run. The runner refuses to report metrics until at least 30 items are human-labelled. See backend/evals/README.md.

■ Known limitations
  • Verified against live services:
    • Gemini (question generation, evaluation, resume profile and job matching).
    • A hosted Supabase project.
    • Live Adzuna job search (real listings, link checks and match scoring).
  • Rate limiting is per process and kept in memory. With several instances, move it to Redis.
  • Synchronous AI calls: generation and evaluation run inside the HTTP request, which takes 10–40 s for generation. A job queue would be the next step at scale.
  • Demo heuristic scores measure only text statistics; they aren't meaningful feedback.
  • React Router v6: two moderate advisories (GHSA-wrjc-x8rr-h8h6, GHSA-337j-9hxr-rhxg) are fixed only in v7.
    • The app doesn't use SSR.
    • Its one redirect from user input (?next=) goes through a strict same-site validator (safeRedirectPath, unit-tested).
    • Upgrading to v7 is recommended.
  • Cost estimates in ai_usage_events use a static price table in app/ai/provider.py. They're approximate.
  • Resume upload: old .doc files and scanned (image-only) PDFs aren't supported; scanned PDFs get a clear message.
■ Requires credentials or manual configuration
  • Supabase project, migrations, Auth URL configuration and email templates/SMTP (email confirmation and password reset).
  • A Gemini or OpenAI API key for real question generation, evaluation and rewriting.
  • Adzuna app_id / app_key for live job search.
  • SUPABASE_SERVICE_ROLE_KEY so account deletion also removes the auth user. Without it, the app deletes all data and tells the user that an admin must remove the login.
  • Render (API, via the Blueprint) and Vercel (web app) projects.
  • Human labels for the evaluation dataset.

API reference  ·  Deployment  ·  Demo script  ·  Evaluator agreement


Prepared by Harsh Gupta   ·   all figures audited by 128 automated tests   ·   ▲ back to top

About

An AI-powered interview coach and job finder. It generates tailored mock interviews from your resume and target job descriptions with rubric-based scoring, and matches your skills with real job listings.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages