LumaCV: AI Resume Intelligence

Problem:
Resume tailoring is a manual, error-prone process that often fails ATS screening due to poor keyword alignment, inconsistent formatting, and AI rewrites that quietly invent facts.
Solution:
Built a free, open-source resume-building platform that extracts resumes client-side, tailors bullets against a target JD across a multi-provider BYOK AI layer, fact-checks every AI edit against the source before it reaches the client, scores the result with a deterministic (non-AI) ATS algorithm, and typesets it natively with Typst.
Impact:
Cut resume tailoring from ~30 minutes to under 60 seconds, with a server-side anti-hallucination validator as the product's core trust guarantee and vector PDFs across 52 ATS-safe templates.
Overview
LumaCV is a free, open-source resume-building SaaS (Next.js 14 App Router, TypeScript, Supabase, Typst). A user pastes or uploads an existing resume, text is extracted entirely client-side (the file never reaches the server), an LLM structures it into typed resume data, the user optionally tailors bullets against a pasted job description, a server-side fact validator strips or reverts anything the AI invented, a deterministic algorithm scores the result against the JD, and the Typst typesetting engine compiles it to a vector PDF. The product's stated design center: "the AI can rephrase, it cannot invent."
Results & metrics
- Reduced resume tailoring turnaround from 30 minutes to under 60 seconds
- BYOK architecture across 4 AI providers plus 5 system fallbacks for 99.9% uptime
- Sub-50ms deterministic ATS scoring algorithm eliminating AI hallucination risks
- 52 pixel-perfect Typst templates generating sub-100KB vector PDFs
Before / after
Resume Tailoring
Before: Manual rewriting + keyword guessing
Trust
Before: Black-box AI rewrites risk inventing metrics or titles
Formatting
Before: Inconsistent Word/Docs formatting
Time to Output
Before: 20-30 minutes
What I built
Tailor and score
Tailors bullets to a pasted JD, then scores the match with no AI call in under 50ms.
Fact validator
Fact-checks every tailored resume server-side; a bullet with an invented metric reverts.
Native Typst PDFs
The Typst CLI typesets a vector PDF in 20-45ms, from 52 ATS-safe templates.
Stack
- Next.js 14 App Router, with a server/client component split for per-page SEO metadata
- Multi-provider LLM orchestration, Gemini, Groq, Mistral, OpenRouter, GitHub Models system fallback; Gemini/OpenAI/Anthropic/Groq BYOK
- Supabase Auth, Postgres + RLS, no ORM (direct PostgREST)
- Zustand, for persistent client state
- Upstash Redis rate limiting, bypassed for BYOK callers
- Native Typst CLI compilation, via child_process, bundled as a platform binary
Decisions
Key trade-offs and design calls that shaped the final delivery.
Native Typst over LaTeX or HTML-to-PDF
Why
- Context
- HTML-to-PDF risks rasterized text and floating divs that ATS parsers misread; LaTeX is precise but heavyweight and slow to compile
- Decision
- Compile via the native Typst CLI (not WASM)—pure vector glyphs, guaranteed top-down reading order, 20-45ms typesetting, at the cost of bundling a platform-specific binary and Node-only (non-Edge) compilation
BYOK plus system fallback, not a single provider
Why
- Context
- A single AI provider outage blocks every user, and free users need a usable default without configuring anything
- Decision
- 4 BYOK providers (Gemini, OpenAI, Anthropic, Groq) for users who bring their own key and bypass the shared rate limit, backed by 5 system fallback providers for everyone else
Deterministic ATS scoring, not another AI call
Why
- Context
- An LLM-scored ATS match is unverifiable and non-reproducible, undermining the product's explainability claim
- Decision
- Edge-rendered, sub-50ms keyword-match formula with dynamic weight renormalization across only the JD categories actually present, so an empty 'preferred skills' list can't cap a well-matched resume at 80%
Server-side fact validation as a non-negotiable invariant
Why
- Context
- LLMs can invent metrics, titles, or employers that a candidate never had, and prompt-level instructions alone aren't a reliable guardrail
- Decision
- Every tailored resume is fact-checked server-side before reaching the client; any bullet containing a metric not traceable to the source resume reverts to the original text entirely, not just the invented number
Architecture
The primary system boundaries, runtime pieces, and how the project was structured in production.
- User Resume / JD → Client-side PDF/DOCX extraction
- Client-side PDF/DOCX extraction → LLM Parser
- LLM Parser → JD Analyzer
- JD Analyzer → Tailoring Engine
- Tailoring Engine → Fact Validator
- Fact Validator → Zustand State
- Zustand State → Deterministic ATS Scorer
- Zustand State → Typst Compiler
- Typst Compiler → Vector PDF
- Supabase Auth + Postgres ↔ Tailoring Engine
- Zustand State ↔ User Resume / JD
- Upstash Rate Limiter → Tailoring Engine
- Next.js 14 App Router + Zustand
Resume Builder Client
How
Multi-step wizard (Upload/Paste + JD → Edit Details → AI Tailoring → Preview + Score) with persistent client state and zero-upload client-side PDF/DOCX text extraction.
- lib/llm-client.ts + lib/fact-validator.ts
AI Orchestration + Fact Validation
How
BYOK across 4 providers plus 5 system fallback providers for resilience; every tailored response is run through a server-side validator that locks employer names, dates, and credentials, and reverts any bullet containing a fabricated metric before it reaches the client.
- Native Typst CLI
PDF Compilation
How
child_process.spawn against a bundled Typst binary—typesetting itself completes in 20-45ms, producing 40-120KB vector PDFs with clean ATS-parseable text, versus rasterized HTML-to-PDF output.
Mermaid source
Paste into mermaid.live to edit the diagram.
flowchart LR
subgraph Input
U[User Resume / JD]
end
subgraph Client
X[Client-side PDF/DOCX extraction]
end
subgraph AI
P[LLM Parser]
A[JD Analyzer]
T[Tailoring Engine]
F[Fact Validator]
end
subgraph Infra
S[(Supabase Auth + Postgres)]
Z[Zustand State]
R[Upstash Rate Limiter]
end
subgraph Output
SC[Deterministic ATS Scorer]
L[Typst Compiler]
PDF[Vector PDF]
end
U --> X --> P --> A --> T
T --> F --> Z
Z --> SC
Z --> L --> PDF
S <--> T
Z <--> U
R --> TPipeline
How changes moved from development through validation and deployment.
Auto-deploy from GitHub; postinstall re-downloads/caches the native Typst binary each build
Sliding-window rate limiting on AI endpoints, skipped entirely for BYOK callers
maxDuration: 60 for AI/tailor routes on Node serverless; Edge runtime for the deterministic ATS score and stats endpoints
Select a step for its detail
Incidents
Operational failures, rehearsals, or recovery moments that changed how the system was run.
- P1
Export success reported on a failed compile
Resolution
Traced through 4 consecutive point releases (2.15.1→2.16.3): removed a fake-PDF fallback string, rebuilt Word export as real OOXML, swept ~60 unguarded .trim() crashes with a safeTrim() helper, and fixed a Zod validation error being handed to the toast layer as a raw object
Lesson: A failed export must fail loudly—never synthesize a fallback file to paper over a real error, and re-examine a 'fixed' bug rather than assuming the first patch found the root cause
- P1
IDOR on resume and application writes
Resolution
Added explicit user_id ownership checks before upsert on any client-supplied resource id
Lesson: Never trust a client-supplied id for ownership—verify server-side on every write, even for authenticated users
- P2
Vercel build silently missing the Typst binary in production
Resolution
Bundled bin/ and typst/ into every API route via outputFileTracingIncludes, since local dev reads the filesystem directly and gives zero signal that production bundling is broken
Lesson: A dev-only code path that happens to work by accident hides real production bundling regressions—verify against the actual deploy artifact, not just local behavior