ZłóżCV
From a job offer link to a tailored CV in 60 seconds
/ Overview
An AI CV generator that turns a job offer link into a tailored PDF in 60 seconds. One source-of-truth database, infinite per-offer variants. If it isn't in the database, it isn't in the CV.
See it live/ Project Details
Category: AI SaaS · own product · PL market
Engagement: In-house product — CraftWeb Labs
Role: Full-stack · AI · PDF pipeline
Timeline: 2026
Live: zlozcv.pl
/ Problem
Every senior dev edits the same CV_v4_final_REAL_FINAL.docx for every offer.
ATS-friendly + visually readable + persona-appropriate — three goals that fight each other. Generic builders ignore matching. "Rewrite my CV" prompts hallucinate skills you don't have.
/ TL;DR
60 s end-to-end
Parse → match → render → PDF. Average run: 56.4 s, exit 0, no hallucinations.
94% match
Average AI match score across 418 generations.
418 CVs
Generated to date. Build-in-public counter.
29 PLN/mo
Pro plan flat price for PL market, no per-CV fees.



/ Vision
One source of truth. Matching is the product. 60 seconds or it's broken. Three principles that survive every prompt revision.
/ Principles
One source of truth
Projects, experience, skills, personas live in one SQLite database. Every CV is a projection.
Matching is the product
The headline number is % match, not "PDF downloaded". Gemini 3 Pro picks the persona, scores every project, renders a bio in your tone.
60 seconds or it's broken
Any longer and the user is back to copy-pasting in Word.
/ Engineering challenges
Puppeteer /dev/shm in Docker. Multi-tenant SQLite slug collisions. LinkedIn 1-click import that survives the next redesign.
Five challenges, five named commits.
/ 01 — 05
01 — Puppeteer /dev/shm hang in Docker
PDF export hung after ~30 s — default 64 MB /dev/shm exhausted. Fix: shm_size: "2gb" + --disable-dev-shm-usage + 400 ms CSS-settle wait.
02 — Multi-tenant slug collision
Global unique constraint on personas.slug broke for second user. Fix: composite per-user constraint uniqueIndex(...).on(table.userId, table.slug).
03 — Structured AI output without function-calling SDK
Function-calling API is ceremonial. Fix: one mega-prompt, raw JSON with strict shape, extractJson() fallback parser, Zod validates post-parse. Full control over retries.
04 — LinkedIn 1-click import that survives the next redesign
Two paths in onboarding-scan.ts — try API first, fall back to a textarea where the user pastes raw profile. Same parser handles both. Extension uses Better Auth bearer.
05 — Stripe webhook idempotency on single-VPS
No Redis, no distributed dedup. Fix: SQLite table stripe_webhook_events with event_id as PK. INSERT OR IGNORE on every event. Doesn't scale past one server — at current scale that's a feature.

/ Stack
Next.js 16 + React 19 + TypeScript strict + Zod 4. SQLite via Drizzle, Better Auth (cookies + bearer), Gemini 3 Pro, Puppeteer for PDF.
One Docker box. Backups = cp.
/ Tech
Next.js 16 + React 19
App Router. Server Actions cut API-route ceremony. Route groups split marketing / dashboard / print.
SQLite + Drizzle ORM
One file in Docker volume. $inferSelect + Zod at AI boundary = one schema end-to-end.
Better Auth
Email/pwd + bearer plugin. Same auth table powers web app (cookies) and Chrome extension (bearer tokens).
Google Gemini 3 Pro
Fast, cheap, returns strict JSON. Anthropic SDK pinned as one-line failover.
Puppeteer + system Chromium
Renders real React Server Component at /cv/print/{id}. WYSIWYG with the dashboard.
Docker + Caddy on VPS
One docker compose up -d. Caddy handles TLS. shm_size: 2gb learned the hard way.
/ Results
Sustainable Pro tier for PL market. Public SaaS + personal cv.kawalec.pl sharing one database.
The thing actually ships.
/ Numbers
94% avg match
Across 418 generations to date.
< 60 s pipeline
Full latency — parse, match, render, PDF.
29 PLN/mo
Sustainable Pro tier vs $20/mo US tools.
2 surfaces
Public SaaS + personal cv.kawalec.pl sharing one DB.










