Skip to content

About

AI-powered interview prep platform - paste a job description and resume to get a personalized skill-gap analysis and prep plan, generated via a RAG pipeline over Gemini embeddings with real-time progress tracking.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

17 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

RoleReady — AI-Powered Interview Prep Platform

React TypeScript Vite Tailwind CSS Spring Boot PostgreSQL Kafka Gemini API Railway Vercel


Live Demo

roleready-interview-prep.vercel.app

Paste a job description (a pre-filled example is loaded for you) and drop in a resume PDF. RoleReady extracts skills from both, retrieves grounded interview questions from a pgvector corpus, and streams a personalized skill-gap breakdown and 3-week prep plan back to the browser live over SSE as each pipeline stage completes — watch the processing screen advance in real time.


Screenshots

Landing page / sign in Resume + job description upload
Landing page / sign in Resume + job description upload
Live SSE progress as the analysis runs Skill gap breakdown and generated prep plan
Live SSE progress as the analysis runs Skill gap breakdown and generated prep plan

What is RoleReady?

RoleReady is an AI-powered interview preparation platform: paste a job description, upload your resume, and it maps the gap between what the role needs and what you already have, then builds a personalized prep plan grounded in a curated corpus of real interview questions rather than freeform LLM guesses. Under the hood it's an event-driven pipeline — job description submission returns immediately while a Kafka consumer does the actual work (skill extraction, RAG retrieval, plan generation) and pushes progress to the browser over Server-Sent Events, so the UI never blocks on a slow AI call.

This is an original project built to explore a Kafka-backed, RAG-grounded generation pipeline end to end, not a tutorial clone.


Architecture

The central design choice is decoupling the request from the AI pipeline: POST /api/analysis writes an analyses row, publishes a jd-submitted event to Kafka, and returns 202 Accepted immediately. A Kafka consumer does the actual (slow, multi-step) work asynchronously and pushes live progress to the browser over SSE.

Client (React)
      ↓  POST /api/analysis  { jdText, resumeId }
Spring Boot API  →  analyses table (status: processing)
      ↓  publish jd-submitted
Kafka (Confluent Cloud, SASL_SSL)
      ↓  consume
JdAnalysisConsumer
      │  SSE: kafka_published
      ├─ skill extraction (Gemini Flash, or reuse parsed resume skills)
      │  SSE: skills_extracted
      ├─ RAG retrieval — embed each skill (Gemini), hybrid search over pgvector
      │  SSE: rag_retrieved
      └─ prep plan generation (Gemini Flash: match score, skill gaps, questions, study plan)
         SSE: plan_generated
      ↓
PostgreSQL (Supabase)  ←────────────→  Browser (EventSource, /api/events/{analysisId})

Backend package layout — a modular monolith, not microservices. Everything runs in one Spring Boot process, but the code is organized into feature packages with a single direction of dependency (no package reaches back into another's internals):

Package Responsibility
auth Email/password signup+login (BCrypt + JWT), GitHub OAuth callback, Spring Security config
user User entity, resume upload, PDF text extraction (PDFBox), Gemini-based skill parsing
jdanalysis JD submission endpoint, Kafka producer + consumer, on-demand skill extraction, analysis history
rag Gemini embeddings, pgvector hybrid search, interview-question corpus ingestion
prepplan Gemini Flash prep plan generation (match score, skill gaps, questions, 3-week study plan)
notification In-memory SseEmitterRegistry + /api/events/{analysisId} SSE endpoint
common JWT utilities, JWT auth filter, Kafka client config, health check

A monolith was chosen deliberately: the pipeline is a single logical workflow (submit → extract → retrieve → generate) with one shared database, and splitting it into services would trade in-process method calls for network calls without buying independent scaling — nothing here has a load profile that justifies it yet. Kafka still sits in the middle so the request and the work are decoupled even though they run in the same process.

Why Kafka instead of an in-process async task. The pipeline makes 1 skill-extraction LLM call, up to 10 embedding calls (one per extracted skill), and 1 plan-generation LLM call — several seconds of sequential I/O. Publishing to Kafka and returning 202 immediately means the HTTP thread is never held open for that; the consumer picks up the message on its own schedule, and the JD payload durably survives a server restart mid-processing, which a @Async method or a raw thread pool would not.

RAG retrieval. For each extracted skill, GeminiEmbeddingService embeds the skill string and QuestionRepository runs a hybrid search: a topic/difficulty-filtered pgvector query (ORDER BY embedding <=> :embedding, an HNSW cosine index) first, falling back to a pure similarity search when the filtered query returns nothing. Results across all skills are deduplicated and capped at the top 10 — so the questions handed to the plan-generation prompt are always real corpus entries, not hallucinated ones.


Key Features

Feature Description Tech
JD + resume analysis Upload a resume PDF and paste a JD; both are parsed into independent skill lists PDFBox text extraction, Gemini Flash skill extraction
Live skill auto-detect JD skill chips re-extract automatically 1s after you stop typing, with a loading state Debounced POST /api/skills/extract
Skill gap detection Resume skills vs. JD-required skills, ranked strong / growing / gap PrepPlanService + Gemini Flash
RAG-grounded prep plan Interview questions retrieved from a real corpus, not generated from scratch Gemini embeddings (768-dim), pgvector HNSW hybrid search
Real-time progress tracking 5-stage pipeline (Kafka published → skills → RAG → plan → push) updates the processing screen live Kafka consumer → SseEmitterRegistry → browser EventSource
GitHub OAuth + email auth Sign in with GitHub or email/password — both issue the same JWT Spring Security, JJWT, GitHub OAuth Apps, BCrypt
Analysis history Every past analysis listed with its status and a link back to its generated prep plan GET /api/analysis/history

Tech Stack

Client Server
Language TypeScript 5.6 Java 21
Framework React 18, Vite 5.4 Spring Boot 3.3 (modular monolith)
Styling Tailwind CSS 3.4 —
Routing React Router 6 Spring MVC
Auth JWT in localStorage Spring Security, JJWT, BCrypt, GitHub OAuth
Database — PostgreSQL (Supabase) + pgvector, Flyway migrations
Messaging — Apache Kafka (Confluent Cloud, SASL_SSL / PLAIN)
AI — gemini-embedding-001 (768-dim), Gemini Flash
Real-time Browser EventSource Spring SseEmitter
File parsing — Apache PDFBox
Deployment Vercel Railway

Why 768-dimensional embeddings instead of Gemini's native 3072. gemini-embedding-001 supports Matryoshka representation learning, so requesting outputDimensionality: 768 returns a truncated-but-still-meaningful vector rather than a degraded one. 768 dims keeps the pgvector HNSW index smaller and queries faster, at a negligible recall cost for a corpus of interview questions that doesn't need maximum embedding fidelity to begin with.

Why the Supabase session pooler, not the transaction pooler. Supabase's transaction-mode pooler (PgBouncer, port 6543) doesn't support session-level features — prepared statement caching and SET commands — that Hibernate relies on. DATABASE_URL points at the session pooler (port 5432) instead, giving full Postgres wire-protocol compatibility for JPA while still pooling connections outside the app.

Why Confluent Cloud for Kafka. A managed, free-tier-friendly Kafka cluster reachable over SASL_SSL means the async pipeline doesn't need a self-hosted broker for a project this size — KafkaConfig just authenticates with PLAIN SASL and connects.


Project Structure

roleready-interview-prep/
├── client/                              # Vite + React + TypeScript frontend
│   └── src/
│       ├── api/                         # auth.ts, analysis.ts, sse.ts — fetch/EventSource wrappers
│       ├── components/                  # NavBar, Avatar, Logo
│       └── pages/                       # Landing, Dashboard, Processing, Results, History, AuthCallback
└── server/                              # Spring Boot modular monolith
    └── src/main/
        ├── java/com/roleready/
        │   ├── auth/                    # signup/login, GitHub OAuth, Spring Security config
        │   ├── user/                    # User + Resume entities, PDF parsing, resume skill extraction
        │   ├── jdanalysis/              # JD submission, Kafka producer/consumer, skill extraction, history
        │   ├── rag/                     # Gemini embeddings, pgvector hybrid search, corpus ingestion
        │   ├── prepplan/                # Gemini Flash prep plan generation
        │   ├── notification/            # SSE emitter registry + /api/events endpoint
        │   └── common/                  # JWT utils, Kafka client config, health check
        └── resources/
            ├── application.yml
            └── db/migration/            # Flyway migrations (pgvector extension, schema)

Local Development

# Prerequisites: Java 21, Node.js 18+, a Postgres instance with the pgvector
# extension available (e.g. a free Supabase project), a Kafka cluster
# (e.g. a free Confluent Cloud cluster), and a Gemini API key.

# 1. Clone
git clone git@github.com:vsingh2201/roleready-interview-prep.git
cd roleready-interview-prep

# 2. Server setup — create server/.env with the variables below
#    (spring-dotenv loads it automatically; no extra config needed)
cd server
./mvnw spring-boot:run

# 3. Client setup (new terminal) — create client/.env with the variables below
cd client
npm install
npm run dev

# 4. Open http://localhost:5173

The server runs Flyway migrations on startup (enabling pgvector, creating tables), so the database just needs to exist and be reachable — no manual schema setup required. Locally, the Vite dev server proxies /api/** to localhost:8080, so VITE_API_URL can stay blank.


Environment Variables

server/.env

Variable Description Example
DATABASE_URL Supabase Postgres connection string — session pooler, port 5432 jdbc:postgresql://aws-0-us-east-1.pooler.supabase.com:5432/postgres
DATABASE_USERNAME Postgres username postgres.xxxxxxxxxxxx
DATABASE_PASSWORD Postgres password —
JWT_SECRET HMAC-SHA256 signing secret for issued JWTs 32+ char random string
GITHUB_CLIENT_ID GitHub OAuth App client ID Ov23ctXXXXXXXXXXXX
GITHUB_CLIENT_SECRET GitHub OAuth App client secret —
GEMINI_API_KEY Google AI Studio API key — used for embeddings and Flash generation AIzaSy...
KAFKA_BOOTSTRAP_SERVERS Confluent Cloud bootstrap server pkc-xxxxx.us-east1.gcp.confluent.cloud:9092
KAFKA_USERNAME Confluent Cloud API key —
KAFKA_PASSWORD Confluent Cloud API secret —
CORS_ALLOWED_ORIGINS Comma-separated allowed origins (default http://localhost:5173) https://roleready-interview-prep.vercel.app
FRONTEND_BASE_URL Where the GitHub OAuth callback redirects with the JWT (default http://localhost:5173) https://roleready-interview-prep.vercel.app

client/.env

Variable Description Example
VITE_GITHUB_CLIENT_ID Same GitHub OAuth App client ID as the server Ov23ctXXXXXXXXXXXX
VITE_API_URL Backend base URL. Leave blank locally — the Vite dev server proxies /api to localhost:8080 https://your-app.up.railway.app

There's no committed .env.example yet — copy the tables above into server/.env and client/.env to get started.


About

AI-powered interview prep platform - paste a job description and resume to get a personalized skill-gap analysis and prep plan, generated via a RAG pipeline over Gemini embeddings with real-time progress tracking.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages