Upload a CV (PDF) and a job description (URL or pasted text) and get an ATS-style match score out of 100, with evidence-based feedback: which requirements are met, which are missing, and concrete suggestions for improving the CV.
The match score is computed deterministically in TypeScript from a fixed, weighted rubric — not by the LLM — so results stay consistent regardless of which model runs the analysis. The LLM is only used to extract structured data from the CV/job text, classify each requirement against the CV with evidence, and write the final natural-language explanation.
Runs entirely locally: no accounts, no database, nothing persisted server-side.
CV PDF ──► text + layout extraction ──► LLM structured extraction ──► CandidateProfile
\
► matching + scoring ──► score/100 ──► LLM explanation
/
Job URL/paste ──► text extraction ──► LLM structured extraction ──► JobProfile
Scoring weights (fixed, sum to 100):
| Category | Weight |
|---|---|
| Required technical skills | 35 |
| Relevant experience | 20 |
| Responsibilities / role intent | 15 |
| Preferred technologies | 10 |
| Seniority alignment | 5 |
| Soft skills | 5 |
| ATS structure / parseability | 10 |
Every requirement is classified as exact, semantic, partial, or missing, each backed by evidence quoted or closely paraphrased from the actual CV — the tool is explicitly constrained to never suggest claiming a skill or experience that isn't there.
- Frontend/backend: Next.js (App Router) + TypeScript, Tailwind CSS, shadcn/ui, Next.js Route Handlers (no separate backend)
- AI orchestration: LangChain.js, giving a shared interface across providers
- Model providers: Groq (hosted) and Ollama (local) — pick whichever's configured at runtime
- Validation: Zod schemas for every structured LLM output (CV/job profiles, requirement matches, ATS layout analysis, final result)
- PDF parsing:
pdfjs-dist, for both text extraction and layout/structure signals (columns, sidebars, tables, non-standard headings) that feed the ATS parseability score - Job URL extraction:
fetch+ Cheerio; falls back to prompting for pasted text if a URL can't be parsed - Tests: Vitest
npm installCopy the example env file:
cp .env.example .env.localThen set up at least one of:
-
Groq (hosted, fast) — get an API key from console.groq.com/keys and set
GROQ_API_KEYin.env.local. -
Ollama (local, private, free) — install Ollama, pull a model, and make sure it's running:
ollama pull llama3.1 ollama serve
OLLAMA_BASE_URLdefaults tohttp://localhost:11434if unset.
The app only offers providers that are actually configured/reachable — check GET /api/providers to see what's currently available.
npm run devOpen http://localhost:3000, upload a CV PDF, provide a job description (URL or paste), pick a provider, and run the analysis.
npm run test # run once
npm run test:watch # watch modeThis app is designed to run locally only. The job-URL fetcher blocks requests to private/internal network addresses (SSRF protection) and CV uploads are capped at 10MB, but there is currently no authentication or rate limiting on any endpoint. If you deploy this publicly, add a rate limiter in front of the API routes first — otherwise anyone with the URL can consume your configured provider's API quota.
app/
page.tsx # main upload/input UI
results/page.tsx # results view
api/cv/extract/ # PDF -> CandidateProfile
api/job/extract/ # URL/paste -> JobProfile
api/analyze/ # matching + scoring + explanation orchestration
lib/
pdf/ # text + layout extraction (pdfjs-dist)
job/ # job URL fetching (Cheerio)
llm/ # LangChain provider abstraction + structured-output extraction
scoring/ # deterministic scoring engine (no LLM)
schemas/ # Zod schemas for every structured type
components/
ui/ # shadcn/ui primitives
ResultsView/ # score breakdown, match list, ATS review, recommendations
tasks/
prd-*.md # product requirements doc
tasks-*.md # implementation task list