Job Application Agent
Agentic AI that analyzes any job description against your resume and tells you exactly where you match, what skills you are missing, and what to learn next. Resume stored as a pgvector knowledge base. Agent runs 3-tool agentic loop. Auto-saves every analysis to an application tracker with status management. Deployed on Railway (server) and Cloudflare Pages (client).
Skill gap analysis — agent compares JD requirements vs resume semantically, not just keyword matching.
Auto-save pattern — every analysis auto-saved to DB with match score, skills, and JD text.
Application tracker — status machine: saved → applied → interviewing → offer → rejected.
Production deployment — Railway (Node.js server) + Cloudflare Pages (React) + Cloudflare Zero Trust (access control).
What It Does
User pastes job description
↓
Agent analyzes in 3 turns:
Turn 1: analyze_job_description → extract required skills
Turn 2: search_resume → pgvector semantic search
Turn 3: generate_analysis → compare + score
↓
Returns:
Match Score: 78%
✅ Matched: React, Node.js, TypeScript, AWS
❌ Missing: GraphQL, Kubernetes
⚠️ Improve: Docker (basic → advanced)
📚 Learn: GraphQL via Apollo docs, K8s via labs
🎯 Assessment: Strong frontend fit, cloud gaps
↓
Auto-saved to applications tracker ✅
Update status as you progress ✅ Resume as Knowledge Base
User pastes full resume text
↓
POST /api/resume
↓
Step 1: Claude reads resume
→ Splits into logical sections:
experience, skills, projects, education
→ Returns structured JSON array
→ Works on ANY resume format ✅
Step 2: Voyage AI embeds each section
→ Model: voyage-3-lite (512 dimensions)
→ Sequential with Bottleneck rate limiter
→ 20s delay between calls (3 RPM free tier)
Step 3: Save to Neon Postgres
→ resume_chunks table
→ Each row: section + content + embedding VECTOR(512)
→ Linked to user_id (multi-user support)
Result: Resume is now semantically searchable ✅
"Do I have AWS experience?" → finds relevant chunks
"What teams have I led?" → finds leadership content Why Claude for Resume Splitting
Option A — Simple regex (## headers): Works ONLY if resume perfectly formatted ❌ Breaks on edge cases ❌ Misses implicit sections ❌ Option B — LangChain text splitters: Splits by character count ❌ No semantic understanding ❌ Splits mid-sentence ❌ Option C — Claude (our approach): Understands resume structure ✅ Works on ANY format ✅ PDF text, Word copy, LinkedIn export ✅ Returns structured JSON ✅ Production grade ✅ Claude prompt: "Split this resume into logical sections. Return ONLY a JSON array. Each item: section name + content. No explanation, no markdown." Temperature: 0 (deterministic) ✅
Agent Tools
Tool 1: analyze_job_description
Input: jd_text (full job description)
Does: Claude extracts structured requirements
Returns: role, company, required skills,
nice-to-have, experience level
Tool 2: search_resume
Input: query (job requirements as search string)
Does: embed query → pgvector cosine similarity
SQL: SELECT section, content,
1 - (embedding <=> $1) AS similarity
FROM resume_chunks
WHERE user_id = $2
ORDER BY embedding <=> $1
LIMIT 5
Returns: top 5 relevant resume sections
Tool 3: generate_analysis
Input: jd_requirements + resume_content
Does: Claude compares both
Returns: matchScore, matchedSkills,
missingSkills, skillsToImprove,
recommendations, assessment Turn-by-Turn Agent Flow
POST /api/analyze { jd: "..." }
↓
Turn 1 — LLM Request:
messages: [{ role: "user", content: "Analyze this JD..." }]
Turn 1 — LLM Response:
stop_reason: "tool_use"
tool: analyze_job_description
input: { jd_text: "..." }
↓
Code executes: Claude extracts JD requirements
↓
Turn 2 — LLM Response:
stop_reason: "tool_use"
tool: search_resume
input: { query: "React TypeScript Node.js AWS" }
↓
Code executes: pgvector semantic search
Returns 5 most relevant resume chunks
↓
Turn 3 — LLM Response:
stop_reason: "tool_use"
tool: generate_analysis
input: { jd_requirements: "...", resume_content: "..." }
↓
Code returns both inputs to Claude
↓
Turn 4 — LLM Response:
stop_reason: "end_turn"
Returns: JSON with match score + analysis
↓
Auto-save to applications table ✅
Return to React UI ✅ DB Schema
-- Resume knowledge base resume_chunks ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id), section TEXT, -- experience / skills / projects content TEXT, -- full section text embedding VECTOR(512), -- Voyage AI embedding created_at TIMESTAMP ) -- Application tracker applications ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id), company TEXT, role TEXT, jd_text TEXT, status TEXT DEFAULT 'saved', match_score INT, matched_skills JSONB, missing_skills JSONB, applied_at TIMESTAMP, updated_at TIMESTAMP ) -- Status machine: saved → applied → interviewing → offer → rejected
Security Implementation
Authentication:
→ JWT tokens (24h expiry)
→ bcrypt password hashing (rounds: 10)
→ Token verified on every protected route
API Protection:
→ Helmet (security headers)
→ CORS restricted to known origins
→ Rate limiting:
General: 100 req / 15 min
Analyze: 10 req / 15 min (Anthropic costs!)
→ Zod validation on all inputs
→ Parameterized SQL queries (no injection)
Access Control:
→ Cloudflare Zero Trust on frontend
→ Only approved emails can access
→ OTP verification via email
→ Protects Anthropic API costs
Error Handling:
→ Production: generic error messages
→ Development: full error details
→ asyncHandler wraps all controllers
→ Global error handler in Express Production Deployment
Server → Railway Platform: railway.app Runtime: Node.js 24 Start: node server/src/index.js Auto-deploy: push to main → Railway deploys Env vars: stored encrypted in Railway ✅ Client → Cloudflare Pages Platform: Cloudflare Pages Build: npm run build (Vite) Output: dist/ Auto-deploy: push to main → Cloudflare builds Env vars: VITE_API_URL = Railway URL Access Control → Cloudflare Zero Trust Policy: email allowlist Auth: OTP via email Protects: entire Cloudflare Pages app Database → Neon Postgres pgvector: resume embeddings SSL: enabled Pooling: connection pooler enabled
What's New vs Projects 1-8
P3 Site Search: → pgvector on product catalog → Static data, no users P9 Job Application Agent: → pgvector on YOUR resume ✅ personal data → Multi-user (each user owns their chunks) → Resume split by Claude (any format) ✅ → Skill gap analysis (not just search) ✅ → Application tracking (state machine) ✅ → Production deployment on Railway ✅ → Cloudflare Zero Trust access control ✅ → Rate limiting to protect API costs ✅
This is the most personal project in the portfolio — built to solve a real problem while job searching. Every feature was driven by actual need: Claude splits resumes because they come in different formats, pgvector finds semantic matches because vocabulary differs between resumes and JDs, the tracker exists because managing applications manually is error-prone. Production deployment on Railway and Cloudflare Pages shows the full journey from idea to live product.