Structured CV review for technical roles, powered by Claude.
cvforge is a CV reviewer tuned for software, ML, and AI engineering roles. Paste your CV, pick the role you're targeting, and get a structured review back — not free-form prose. The output is the same shape every time: a score, ranked issues with severity, missing signals for the role, ATS keyword gaps, quantification opportunities, and a section-by-section verdict.
The technical bet: a generic "AI resume reviewer" gives bland advice because it serves everyone. A reviewer that knows what an ML engineer's CV should look like — production deployment, evaluation methodology, monitoring — produces dramatically better feedback than ChatGPT with a vague prompt.
Add your Vercel URL here once deployed.
See /example for the rendered review shape (a
full sample is baked into lib/sample-review.ts).
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 14 (App Router) | Server Actions + RSC give tight client/server boundaries for free |
| Language | TypeScript strict (no any) |
Reviews flow through Zod schemas; types are the contract |
| UI | Tailwind + lightweight UI primitives | Owned source, dark by default, design tokens in CSS variables |
| Auth + DB | Supabase (Postgres + Auth + RLS) | RLS does authorization at the row level, not in app code |
| Storage | Supabase Storage (Pro) | Saved CVs and revision history live here, behind RLS |
| Billing | Stripe Checkout + Customer Portal | We don't custom-build account/billing flows |
| LLM | Claude (Sonnet 4.5) via official SDK | Tool use forces structured JSON output — no prompt-engineered prose |
pdfjs-dist |
Parse PDFs in the browser, only extracted text is sent to the server | |
| Rate limit | Upstash Redis + @upstash/ratelimit |
Free-tier IP/fingerprint limiting on the public endpoint |
| Observability | Sentry + Vercel Analytics | Error tracking + traffic, both first-party |
| Hosting | Vercel + Supabase Cloud | Closest match for the stack, zero-config previews |
┌────────────────────────┐
│ Browser (Next.js RSC) │
│ - PDF text extraction │
│ - Supabase auth client│
└─────────┬──────────────┘
│ POST /api/review
▼
┌───────────────────────────────────────┐
│ Next.js Route Handlers │
│ /api/review → Claude tool use │
│ /api/checkout → Stripe │
│ /api/portal → Stripe billing │
│ /api/webhooks/stripe (signed) │
│ /auth/callback → Supabase OAuth │
└──────┬─────────────────────┬──────────┘
│ │
▼ ▼
┌─────────────────┐ ┌────────────────┐
│ Supabase │ │ Anthropic API │
│ - Auth (email + │ │ - submit_review│
│ Google OAuth) │ │ tool returns │
│ - Postgres+RLS │ │ strict JSON │
│ - Storage │ └────────────────┘
└─────────────────┘
▲
│ webhook (signed, raw body)
┌───────┴─────────┐
│ Stripe │
└─────────────────┘
This is the core design choice that makes cvforge different from a ChatGPT wrapper.
The Claude call is a tool-use call (lib/claude.ts).
We define one tool, submit_review, whose input_schema is a strict JSON
Schema mirroring the Zod schema in lib/schemas/review.ts.
We force tool_choice to that tool, so Claude's only legal response is to
call it exactly once. We never parse free-text output — and after the call
we re-validate the tool input through Zod, so a model surprise still gets
caught at the type boundary.
The system prompt is composed of three layers:
- Base prompt (
lib/prompts/system.ts) — the reviewer persona, calibrated score rubric (90+ would forward to hiring manager, 0–39 not currently competitive), and hard rules ("severity must be justified by role context", "every issue must quote exact CV text", "rewrites must be drop-in replacements using only facts in the original quote", "never invent achievements"). - Role rubric (
lib/prompts/roles/) — per-role signals to weight, things to penalize hard, common missing signals to surface, and ATS keywords. The ML engineer rubric checks for production deployment, evaluation methodology, monitoring. The frontend rubric checks for Core Web Vitals, accessibility, design-system contributions. Each rubric is short, specific, and editable in isolation. - User context — the CV text, optional job description, target role.
This separation means we can iterate on one rubric without regressing
others, and we snapshot-test prompt construction in
tests/prompts.test.ts.
The output schema enforces:
- Every issue includes a verbatim quote from the CV (so the user can find it).
- Every issue includes a drop-in rewrite — the actual line they should paste back, not advice like "add metrics".
- Severity is one of three values —
critical/major/minor— so the UI can render and sort consistently. - Missing signals and keyword gaps are bounded so the model can't dump a laundry list.
A diff against ChatGPT-with-a-prompt looks like this:
| ChatGPT free-form | cvforge structured |
|---|---|
| "Try to add more metrics to your bullets." | An exact quoted bullet + a rewritten version with metrics |
| "Your skills section could be improved." | "Skills" verdict ≤15 words + the specific reorganization |
| Long mixed prose hard to act on | UI components per field, sortable by severity |
| Hallucinated metrics | Schema rule: rewrites use only facts in original quote |
git clone https://github.com/<you>/cvforge.git
cd cvforge
cp .env.example .env.local # fill in keys
npm install
npm run devYou'll need:
- Supabase project (free tier is fine). Apply the migrations in
supabase/migrations/via the Supabase CLI or paste them into the SQL editor in order (0001 → 0002 → 0003). - Anthropic API key. The default model is
claude-sonnet-4-5-20250929. - Stripe account in test mode + a recurring Pro price configured.
- Upstash Redis instance for rate limiting the anonymous endpoint.
For Google OAuth, configure the provider in Supabase Auth and set the redirect
URI to ${SITE_URL}/auth/callback.
- Push to GitHub and import into Vercel.
- Paste the env vars from
.env.exampleinto the Vercel project. - Configure the Stripe webhook to point at
https://<domain>/api/webhooks/stripewith signing secret inSTRIPE_WEBHOOK_SECRET. - Run the Supabase migrations against your production project.
- Set
NEXT_PUBLIC_SITE_URLto the production URL so OG metadata, OAuth redirects, and Stripe success URLs resolve correctly.
app/
├── page.tsx # landing + free anonymous review flow
├── example/page.tsx # public sample review
├── pricing/page.tsx
├── privacy/page.tsx
├── terms/page.tsx
├── auth/login/ # email/password + Google OAuth
├── auth/callback/route.ts
├── api/
│ ├── review/route.ts # validated input → Claude → DB
│ ├── checkout/route.ts # Stripe Checkout session
│ ├── portal/route.ts # Stripe Customer Portal
│ └── webhooks/stripe/route.ts # signed webhook handler
└── dashboard/
├── page.tsx # review history
├── billing/ # plan + portal actions
└── reviews/[id]/page.tsx
└── reviews/compare/page.tsx # Pro: side-by-side compare
components/ # CvInput, ReviewOutput, ScoreRing, etc
lib/
├── claude.ts # tool-use Anthropic call
├── prompts/ # base + per-role rubrics
├── schemas/ # Zod schemas (the contract)
├── supabase/ # SSR + browser + admin clients
├── stripe.ts
├── ratelimit.ts
└── billing.ts
supabase/migrations/ # schema + RLS + anon-purge cron
tests/ # vitest
- Supabase schema + RLS policies + anonymous-purge cron
- Zod review schema (the contract)
- System prompt + role rubrics + tool-use Claude call
- Unauthenticated review flow on
/ - Auth (email + Google) + dashboard
- Stripe billing + webhook + tier enforcement
- PDF upload (client-side parse)
- Revision compare view (Pro)
- Landing page polish, pricing, privacy, terms, /example
- CI (lint, typecheck, tests, build), Sentry, Vercel Analytics
- Deploy to Vercel + production Supabase + live Stripe
- PDF export of review reports (Pro)
- Post-launch: prompt eval harness with golden CVs
MIT — see LICENSE.