Skip to main content
Back to Canvas
Project2026Product EngineerNext.jsSupabaseAI IntegrationFull StackLocalization

Building an AI-Powered Job Search Workspace

A full-stack job tracker that extracts resume text client-side, scores it against job descriptions via LLM, and keeps API keys off the frontend — all with no backend footprint beyond Supabase and Vercel.

Job hunting at volume is a project management problem: dozens of tailored resumes, each tweaked for a different JD, and no reliable way to remember which version went where. CVant is a workspace that solves exactly that — track applications, manage resume versions, and let an LLM tell you how well each resume matches the role. No duct tape, no spreadsheet, no "half the context is buried in a Discord thread."

The dashboard is intentionally sparse: one card per application, status chip, and an analyze button that does the heavy lifting. Expand a card to see the full JD, the attached resume, and — if you've run analysis — the match score with matched keywords, missing keywords, gap notes, and concrete suggestions. Statuses flow from not appliedappliedinterviewingoffer / rejected; archive cleans up closed loops so the active list stays signal.

Dashboard with application cards and status tracking

AI Analysis

The analyze flow had one hard constraint: API keys must never touch the browser. So resume text is extracted client-side via PDF.js, PII (emails, phone numbers, addresses) is stripped before it leaves the device, and the sanitized text is capped at 20,000 characters for cost control. That payload — resume text + JD + locale — is sent to a Supabase Edge Function running on Deno, which holds the OpenRouter key and owns the billing logic.

The function validates the user's JWT, checks their credit balance (5 free analyses, then Stripe-purchased credits), calls the LLM, and writes results back to the applications table. A primary model handles most requests; a fallback kicks in automatically on timeout so analysis rarely visibly fails. The prompt enforces strict JSON output; the Edge Function strips markdown fences and validates the schema with Zod before touching the database.

AI match score with keyword analysis and suggestions

Billing & Idempotency

Stripe handles credit purchases. The webhook route checks stripe_session_ids — an array column on the profiles table — before granting credits, so retried webhooks are no-ops. The Edge Function mirrors this: credit deduction and free-tier counter increment happen inside the same transaction as the results write, so a timeout can't produce an analysis without consuming a credit or vice versa.

Credit purchase modal and account usage display

Stack, data & locale

Auth, database, storage, and the AI Edge Function all live in Supabase. Row Level Security policies mean every query is already scoped to the authenticated user — no WHERE user_id = ? scattered through API routes. Resumes are stored in a private bucket; download URLs are signed and time-limited, generated server-side per request.

i18n via next-intl: locale from cookie or Accept-Language, kept in the URL. The Edge Function reads a responseLanguage cookie and threads it into the prompt so Chinese users get AI feedback in Chinese without post-processing. Theme via next-themes; animations via Anime.js with staggered card reveals and smooth expand/collapse — all behind a prefers-reduced-motion check.

Tech: Next.js 15 App Router, React 19, Tailwind CSS 4, shadcn/ui, Zod end-to-end (client validators + Edge Function schema checks), TypeScript generated from the Supabase schema.

If I extend it: optional resume diff view between versions, a "copy as text" export for pasting into cover letters, and a public share link for job seekers who want feedback from peers.