Building VettaCV: a resume tailor that grew into a job-search platform
Engineering notes on taking a Streamlit prototype to production: one LLM schema, a tight token budget, a lean API, Firestore, Razorpay and AWS. Five-minute read.
Built in the order things broke
Three deployables, one origin for the browser
One Pydantic schema is the backbone
Early versions asked the model for JSON and validated it afterwards. That works until it doesn't. Moving to
Structured Outputs (pass a Pydantic model, get a guaranteed match) made one Resume schema the
contract between parsing, the editor, the PDF, tailoring and job matching.
# the resume schema (abridged) class WorkExperience(BaseModel): name: Optional[str] # company position: Optional[str] startDate: Optional[str] endDate: Optional[str] description: Optional[str] class Resume(BaseModel): basics: Optional[BasicInfo] workExperiences: Optional[List[WorkExperience]] education: Optional[List[Education]] skills: Optional[Section] completion = client.responses.parse(model=MODEL, input=messages, text_format=Resume) resume = completion.output_parsed # typed, no json.loads, no retries for shape
Guardrail: tailoring may rephrase, reorder and emphasise. It never adds experience, tools or numbers. The UI shows a word-level diff so the user approves every change.
The cheapest token is the one you don't send
| Technique | What it saves |
|---|---|
| Schema instead of examples | No JSON templates or format rules in the prompt: the schema is the instruction. |
| Compact resume context | Prompts get trimmed plain text (sections + bullets), not the raw document. |
| Small models only | gpt-4o-mini / o4-mini / gpt-4.1-mini. Parsing moved off a reasoning model: no hidden reasoning tokens. |
| Code before LLM | Job import (ATS APIs, JSON-LD), salary parsing, country detection and filters are regex and plain code. |
| Shared caches | Company interview guides are researched once per company × role family and reused for 30 days: ~$0.03–0.06 per guide, not per user. |
| Charge on success | Credits are deducted only after a generation succeeds; retries never double-charge. |
A small Flask monolith, kept lean on purpose
One deployable, one log, one process to restart. Running on a small instance shaped the design more than any framework did:
Firestore, organised around the user
Firebase gave auth, a document store and file storage with zero servers. A resume is one nested, evolving JSON object, which fits a document model. The rules we kept:
- Everything belongs to one account: resumes, saved jobs, filters, applications and credits sit under the signed-in user.
- Snapshots, not joins: a mock-interview request copies the candidate's years, companies and title, never name or email, so privacy is decided at write time.
- Caches are collections with a version stamp, so a prompt or model change invalidates them cleanly.
- Simple queries, smart memory: location, salary and recency filters run on an in-memory index instead of composite Firestore queries.
Razorpay: the server owns the price
The editor, and guardrails for vibe coding
The editor was the hardest UI: WYSIWYG, print-perfect on A4 and Letter, autosaved locally, and able to show AI changes as accept/reject diffs. Open-source JSON resume builders didn't fit that flow, so it's custom.
AI coding tools let a tiny team ship a lot of UI, and break a lot of UI. What kept it shippable:
- One change per PR, so a bad change is a single revert.
- Local preview pages with sample data to check every UI state without logging in.
- Typecheck, lint and tests before merge, and reading every generated diff.
Domain, DNS, SEO
| Piece | Choice |
|---|---|
| Domain / DNS | Bought at a registrar, DNS managed on AWS so records live next to the services they point at. |
| Deploys | The app builds and deploys on every push to main; the landing site is a static export on a CDN. |
| Transactional email from the domain, authenticated with SPF, DKIM and DMARC so it lands in the inbox. | |
| SEO | Fixed robots, canonical URLs, redirects, breadcrumb schema; programmatic /resume-tips/[jobTitle] pages. |
| AEO | Q&A-shaped content AI assistants can cite. AI assistants are now the #2 traffic source after direct. |
How it's doing (GA4, Oct 2026)
What I'd tell myself in 2024
Resume schema saved more time than any framework choice.