LumaCV: AI Resume Intelligence
- 30 min -> <60 sec tailoring
- BYOK across 4 providers + 5 system fallbacks
- Deterministic ATS scoring (no AI call, <50ms)
- 52 Typst templates, 5 export formats

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."
Before / after
Resume Tailoring
Before: Manual rewriting + keyword guessing
After: AI-assisted JD alignment with a fact-checked audit trail
Trust
Before: Black-box AI rewrites risk inventing metrics or titles
After: Server-side validator reverts any bullet with an unverifiable metric
Formatting
Before: Inconsistent Word/Docs formatting
After: Deterministic, native Typst-generated vector PDFs
Time to Output
Before: 20-30 minutes
After: <60 seconds
Stack
Decisions
Key trade-offs and design calls that shaped the final delivery.
Native Typst over LaTeX or HTML-to-PDF
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
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
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
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.
Next.js 14 App Router + Zustand
Resume Builder Client
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
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
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 visualize 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.
Build
VercelAuto-deploy from GitHub; postinstall re-downloads/caches the native Typst binary each build
Rate Limiting
Upstash RedisSliding-window rate limiting on AI endpoints, skipped entirely for BYOK callers
Deploy
Vercel (Node + Edge)maxDuration: 60 for AI/tailor routes on Node serverless; Edge runtime for the deterministic ATS score and stats endpoints
Incidents
Operational failures, rehearsals, or recovery moments that changed how the system was run.
Export success reported on a failed compile
P1Resolution: 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
IDOR on resume and application writes
P1Resolution: 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
Vercel build silently missing the Typst binary in production
P2Resolution: 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