← Selected work
RAG · pgvector · Skill Gap Analysis · Project 9

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).

Anthropic Claude · Neon Postgres · pgvector · Voyage AI · Node.js · React · Railway Live Demo ↗ GitHub ↗
New Concepts This Project
Resume as RAG knowledge base — Claude splits resume into sections, Voyage AI embeds each, stored in pgvector for semantic search.
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 ✅
Why This Project Matters

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.