Building Jobprep: An AI Platform That Preps You for the Job and Applies for you
Summary
Jobprep is an end-to-end AI job-search platform built around the idea to prepare you for the job and apply to it, based on your resume.
Upload your document and it parses into a rich, structured profile. From there, two engines go to work.
The preparation engine evaluates your resume with strengths and gaps, analyzes your fit against a specific job description (calibrated match score, evidence-backed matches, severity-rated gaps, red flags an interviewer would catch), generates interview questions tailored to your background and that role, runs scored mock interviews across clarity, depth, structure, and evidence, and tests your knowledge with seniority-calibrated assessments.
The application engine discovers matching roles, then autonomously fills and submits real employer application forms, reading validation errors and correcting itself on retry across a server-side browser worker.
A Chrome extension that runs in your own browser, and a mobile reference-panel fallback. When a form is too custom to finish safely, it hands off cleanly rather than guessing or fabricating.
This post walks through the whole build: scope, requirements, and the architecture (Next.js, Flutter, a Chrome extension, Supabase/Postgres, and an EC2 Playwright worker, tied together by Clerk and language models). It covers how each piece actually works, from resume parsing to the apply state machine's self-correcting error loop and it's candid about the limits: custom JavaScript upload widgets that defeat automation, job sources that skew technical, and prep features still carrying tech-role assumptions.
An assistant that prepares you for a job and applies for you reliably, and knows when to stop.
The Problem
Applying to jobs is a grind of repetition. You upload the same resume, retype the same contact details, paste the same experience into subtly different forms, and answer the same screening questions across dozens of employer portals, each with its own layout, quirks and idea of what a "required field" is.
But applying is only half the battle. The other half is being ready, knowing whether you're actually a fit for a role, understanding your own gaps before an interviewer finds them, and having practiced the questions you're likely to face. Most tools do one or the other.
I wanted a single system that did both: prepare you for the job, then apply to it.
Scope & Use Case
Jobprep is built around one profile that powers two engines.
The shared foundation:
Upload a resume. The system parses it into a rich, structured profile, the single source of truth everything else draws on.
The preparation engine:
Evaluate. The system reads the resume and returns an honest assessment, strengths to lean on, gaps worth closing.
Analyze fit. Against a specific job description, it produces a calibrated match score, evidence-backed strong matches, real gaps, talking points, and red flags an interviewer might catch.
Generate questions. It writes interview questions tailored to this candidate and this role, not generic prompts, but questions that probe your actual resume items and the JD's specific focus areas.
Mock interview. It runs a conversational practice interview and scores your answers across clarity, depth, structure, and evidence.
Assess. It tests underlying knowledge with multiple-choice assessments calibrated to seniority.
The application engine:
Discover matching jobs. Roles are pulled from job sources and ranked against your profile.
Apply with AI. For a chosen job, an agent opens the application page, fills the form from your profile, and submits it.
Hand off gracefully. When the agent hits something it can't safely resolve, it stops and hands the last step to you rather than guessing.
The primary user is a job seeker who wants to be both prepared and prolific, someone who wants to walk into interviews sharp and apply to many roles without the drudgery.
The product spans a Next.js web app and a native mobile app (built in Flutter), sharing one backend and one identity system.
A deliberate scoping decision: Jobprep began life as a developer-focused tool, its earliest features assumed technical roles (interview-question generation, skill taxonomies, coding-style assessments). Over time it grew toward being profession-agnostic, driven by whatever the user's profile actually says.

Requirements
Functional | Non-Functional |
|---|---|
Parse PDF/DOCX resume into a structured profile (contact, experience, skills, projects, education, inferred role, seniority, strengths, gaps). | The system must never fabricate employment, dates, or credentials to make an application "go through." Ground truth is the resume, anything not derivable from it is omitted, not invented. |
Evaluate the resume and surface honest strengths and gaps. | A form the agent can't finish should end in a clean handoff, never a broken or dishonest submission. |
Analyze candidate-to-role fit against a specific job description, with a calibrated score, evidence, gaps, and red flags. | Resume and form parsing use a cheaper, faster model, reasoning-heavy steps (fit analysis) use a stronger one. |
Generate interview questions tailored to the specific candidate and role. | The worker that drives browsers must survive crashes and disconnects and keep processing jobs. |
Run scored mock interviews with multi-dimensional feedback. | Marketing consents are off by default. Only required consents are auto-accepted. |
Generate and grade seniority-calibrated multiple-choice assessments. | |
Aggregate job postings from multiple external sources and keep them fresh. | |
Rank postings against a user's profile. | |
Drive a real browser to fill and submit an application form. | |
Detect form-validation errors after a failed submit and attempt to resolve them. | |
Fall back to a human handoff when automated completion isn't safe or possible. | |
Let users edit every section of their parsed profile. | |
Work across web and mobile with a shared account. |
Architecture
Jobprep is four cooperating systems.
1. The Web App (Next.js)
A Next.js App Router application with TypeScript and Tailwind, deployed on Vercel. It hosts the dashboard, profile management, job discovery, and all the server-side API routes. Authentication is handled by Clerk. Data lives in Supabase Postgres, accessed through the Drizzle ORM. Uploaded resume files are stored in Supabase Storage.
2. The Mobile App (Flutter)
A native iOS/Android app built in Flutter, sharing the same Clerk identity and the same backend via a dedicated set of API endpoints.
Because a phone can't run a browser extension, the mobile experience centers on server-side apply ("Apply with AI") plus a reference-panel handoff: when a form needs the user, the app shows what was filled and opens the employer page in the browser.
3. The Browser Extension (Chrome)
On the desktop, Jobprep ships a browser extension that acts as the counterpart to the mobile app's server-side apply. Instead of a remote worker driving a headless browser, the extension runs inside the user's own logged-in browser, which sidesteps a whole class of problems that block server-side automation: existing employer-site sessions, cookies, and anti-bot walls that treat a datacenter IP with suspicion.
When the user lands on an application page, the extension reads the form, requests filled values from the backend (using the same profile and the same resolution logic as the worker), and populates the fields right there in the tab. Because the fill happens in a real, human browser session, it reaches forms the headless worker can't. The user reviews and submits, the extension fills, the human confirms.
Architecturally, the extension and the worker share the same backend brain: the endpoints that turn a field list plus a profile into truthful values, and that resolve validation errors after a failed submit, serve both. The difference is only where the browser lives, on the user's machine (extension) or on the server (worker).
4. The Data Layer (Postgres + Drizzle)
A single schema underpins everything: users (with per-user API keys and source preferences), resume, profiles, job descriptions and their parsed/gap-analyzed forms, generated interview questions, mock-interview sessions with transcripts and scoring, MCQ topics/questions/attempts, job postings, and the auto-apply job queue.
Notably, job postings are a shared global pool, postings have no owner, instead, matching ranks them per-user at query time. The assessment questions are similarly pooled and deduplicated, so a question generated for one user's target topic can be reused for another's.
5. The Worker (EC2 + browser automation)
(The heart of the "apply with AI" capability) A long-running Node process on an EC2 instance that drives a headless browser (Playwright/Chromium). It pulls queued application jobs, navigates to each form, fills it from the user's profile, attempts submission, resolves errors, and reports the outcome back.
It runs as a systemd service so it survives crashes, reboots, and SSH disconnects.

How It Works
Resume Parsing
When a resume is uploaded, the backend extracts raw text (using pdf-parse for PDFs and mammoth for Word documents), then sends that text to a language model with a schema-constrained prompt. The model returns a structured profile validated against a strict schema, not just a flat skill list, but narrative detail: quantified achievements, inferred role, seniority, and an honest read of strengths and gaps. That richness is deliberate: every downstream feature (matching, fit analysis) is only as good as the profile behind it.
The parsed profile is stored as JSON alongside a record of the source file. The full pipeline is reused identically on both web and mobile, so a CV uploaded from a phone produces exactly the same profile as one uploaded from the browser.

The Preparation Engine: Getting Ready:
The preparation engine turns that structured profile into genuine interview readiness through several stages, each building on the last.
Resume evaluation, strengths and gaps:
The first thing the system does with a parsed profile is tell you the truth about it. Rather than flattering "you have React experience", it identifies specific things an interviewer would dig into:
("led a migration from monolith to microservices") and, just as importantly, names the gaps, "no testing mentioned anywhere, no system-design signals, vague impact claims".
The philosophy throughout is that sycophancy makes these tools worthless, the value is in the honest read.
Fit analysis, you vs. this specific job:
When you point the system at a particular job description, it first parses that JD into structured signals not just a skills checklist, but the things an interviewer would actually probe: what the posting emphasizes repeatedly, what it lists first, the seniority cues, the inferred daily reality of the role. Then it runs a gap analysis bridging your resume and that JD. The output is deliberately blunt: a calibrated match score (90+ is a near-slam-dunk. 50–60 means a tough interview ahead), evidence-backed strong matches that quote your actual companies and projects, gaps rated by severity (critical / moderate / soft) each with a suggested way to bridge it, talking points where your resume undersells you, and red flags an interviewer might raise. This is the reasoning-heavy step, so it uses a stronger model than the parsing stages.
Tailored interview questions:
From the profile, the parsed JD, and the gap analysis together, the system generates a focused set of interview questions this specific candidate is likely to face for this specific role. The bar is specificity: a generic "tell me about a project" is a failure, "walk me through the payments integration you led at that company, specifically how you handled webhook reliability" is the target. Questions span behavioral, technical, system-design, culture-fit, and deep-dive kinds, each carrying a rationale (why this question for this candidate), an ideal-answer hint so you can self-grade, and, for deep dives, the exact résumé item it probes.
Mock interviews with scoring:
Questions become practice. A mock-interview session runs as a real back-and-forth, the system asks, you answer, it follows up, in one of several modes (a quick single-question deep-dive, a mid-length mixed set, or a full structured loop). When the session completes, it scores you across four dimensions, clarity, depth, structure, and evidence, with an overall grade, a summary, your strong and weak moments, and concrete suggestions.
The transcript is preserved so you can review exactly what you said and how it landed.
Assessments. Alongside the conversational interview, the system generates multiple-choice assessments calibrated to seniority, drawn from a deduplicated global pool of questions organized by topic. Attempts are scored and preserved, and the question bank improves over time as more are generated and answered. This tests the underlying knowledge that a conversation might not surface.
Taken together, the preparation engine means that by the time you apply to a role, you already know your fit, your gaps, the questions you'll face, how your answers score, and where your knowledge is thin. The application engine then handles the applying.

Job Discovery & Matching
Postings are synced from several free sources, Remotive (remote tech roles), Arbeitnow (EU/remote), and Platsbanken (the Swedish national employment board, which covers every profession).
A sync process fetches postings, normalizes them into a common shape, extracts lightweight skill tags, and upserts them into the shared postings pool. Stale postings age out of results by a recency filter.
Discovery then ranks that pool against the user's profile and presents the best matches, each with a match score, matched skills, and a route to apply.
Applying with AI
This is the ambitious part. When a user chooses "Apply with AI," a job is enqueued, and the worker picks it up and runs a sequenced routine:
Navigate to the application URL.
Check for headless blocks (anti-bot walls) and safety (is this a form we should touch?).
Extract fields from the page, inputs, selects, radio/checkbox groups, file inputs — with their labels and options.
Resolve values by sending the field list and the user's profile to a model, which returns truthful values for each field it can fill.
Apply those values to the DOM, dispatching the right events so the page's own JavaScript registers them.
Handle files, cover letters, and consents, attach the résumé, generate/attach a cover letter where appropriate, accept required consents.
Submit, and on failure, retry, read the validation errors the form displays, send them back to the model to resolve specifically what's missing, re-apply, and try again up to a small limit.
Hand off if it still can't complete, with a real reason, not a silent failure.

The Error-Resolution Loop
A form rarely fills perfectly on the first pass. A dropdown needs a specific option, a field the profile didn't cover is suddenly required. So after a failed submit, the worker reads the on-screen validation errors, re-reads the current state of the page (fields can appear after an action, for instance, a file-category selector that only exists once a file is attached), and asks the model to resolve just those errors against the profile and the now-current fields. The corrected values are merged in and the form is re-submitted.
Two Ways to Apply: Extension vs. Worker
The server-side worker is fully autonomous: the user taps "Apply with AI," closes their laptop, and the worker does the whole thing remotely. Maximum convenience, but it runs from a datacenter, in a fresh headless browser with no prior relationship to the employer's site, which is exactly what modern anti-bot systems are tuned to catch.
The browser extension trades autonomy for reach: it runs in the user's real, already-authenticated browser, so it inherits their sessions and looks like an ordinary human visitor. It handles forms the worker can't, but it needs the user present to review and submit.
Crucially, both call the same backend endpoints to turn a form plus a profile into truthful values, and to resolve validation errors after a failed submit. The "brain" is written once, the extension and the worker are just two bodies it can inhabit. On mobile, where neither an extension nor a full browser is available, the experience falls back to a reference-panel handoff: the app shows what would be filled and opens the employer page for the user to complete.
Durability & Operations
The worker runs under systemd with automatic restart, so a crash or a dropped SSH session no longer takes the whole apply capability down, a failure mode that caused a lot of early confusion before it was diagnosed as simply the process dying with the terminal. Logs stream to the journal (or a file) for live debugging, and each job's progress is traceable step by step.

Limitations
Automated apply degrades on non-standard forms:
Standard HTML forms with normal inputs are the happy path. But many employer portals use custom, JavaScript-rendered widgets, drag-and-drop file zones, category pickers built from JS arrays rather than real <select> elements, "apply with LinkedIn/OneDrive/Dropbox" integrations, and multi-step flows behind login walls. A generic field-filler cannot reliably drive these. When the agent meets one, it does the right thing, it hands off, but that means the fully-automatic promise is really "automatic where the form is standard, assisted where it isn't."
One concrete example: a widely-used Swedish apply platform renders its file-category selector only after a file is uploaded, and builds it as custom DOM from a JavaScript variable, invisible to standard field extraction until you write a handler specifically for it.
The job sources are tech-skewed:
Two of the three sources are effectively tech-job boards. Even after making the sync profile-driven, so an artist's profile searches for artist roles rather than a hardcoded list of developer titles, the underlying catalogs simply don't carry many non-technical roles. The Swedish national board is the one source with genuine cross-profession breadth. So while the matching is profession-agnostic, the supply still leans technical, and serving non-tech users well is ultimately a sourcing problem, not a matching one.
Tech assumptions linger in the deeper features:
The resume-tailoring, interview-prep, and job-description-analysis features were written with technical roles in mind, down to prompts that literally frame the task as analyzing "an IT role." Making the whole system truly general-purpose means revisiting those, not just the sync.
Third-party SDK maturity:
The mobile authentication is built on a beta SDK whose method signatures shift between releases, so custom auth flows require pinning versions and verifying calls against the installed build.
Auto-attaching a tailored resume is only partly solvable:
Attaching a generated PDF works on standard file inputs but not on the custom uploaders described above, the same wall, from a different direction.
The extension widens reach but not to everything:
Running in the user's own browser defeats session and anti-bot barriers the worker trips over, but it doesn't magic away custom JavaScript widgets or multi-step OAuth application flows. It also requires the user to be at their desk, in Chrome, so it complements, rather than replaces, the mobile and server-side paths. The three surfaces (worker, extension, mobile reference-panel) are best understood as a spectrum from fully autonomous but limited reach to fully manual but universal.

What I Learned
The unglamorous infrastructure decides whether the clever part works. The most sophisticated apply logic is worthless if the worker process dies when your SSH session times out. Durability (systemd, restart-on-crash) mattered more to the actual user experience than any prompt. The allocation of resources for browser for automations is crucial for smooth system operations.
Status 200 is not "it worked." An endpoint can respond successfully while its output never reaches the place that needs it. Tracing data end to end, not just checking status codes, is where real bugs hide.
State changes under you. Forms reveal new fields after actions. Re-reading the page's current state before resolving errors, rather than trusting a snapshot taken earlier, was the difference between three identical failed attempts and an actual fix.
Match the tool to the data shape. LLM field-resolution is powerful for standard fields but the wrong tool for bespoke JavaScript widgets, which need targeted DOM automation. Knowing which problem you have prevents a lot of wasted effort.
Conclusion
Jobprep set out to answer a question: how much of the job search both getting ready and getting in, can AI actually take off your plate?
The answer turned out to be nuanced and, I think, more interesting than a simple "all of it."
On the preparation side, the system genuinely delivers: it reads a resume, tells you honestly where you're strong and exposed, analyzes your fit against a real job with evidence and calibrated scoring, drills you with questions tailored to your actual background, runs scored mock interviews, and tests you with assessments. This half works reliably because it's bounded, it reasons over text you provide and returns text, with no messy external world to fight.
On the application side, for the well-behaved majority, parse a resume, understand a candidate, rank roles, fill a standard form, resolve its errors, and submit, the system also works, across web and mobile, end to end. That covers a real and useful slice of the problem.
For the messy minority, custom upload widgets, login-walled portals, OAuth-based applications, professions the job sources barely cover, the honest outcome is a graceful handoff: the agent does everything it safely can and hands you the last mile, rather than faking a submission or fabricating an answer. That restraint is a feature, not a shortfall. An automation tool you can trust not to lie on your behalf is more valuable than one that maximizes submissions at the cost of truth.
The bigger takeaway is architectural. The value wasn't in any single clever component but in a shared foundation feeding two engines: one profile, parsed once, powering both a preparation loop (evaluate, analyze, question, interview, assess) and an application loop (discover, fill, resolve, submit or hand off). The preparation engine is where the honest-reasoning philosophy lives, it's only useful if it refuses to flatter you. The application engine is where the messy-real-world engineering lives, durability, error-resolution, graceful degradation. Building both reliably, on one coherent profile, was the real work. The AI calls were the easy part, making the whole system trustworthy, honest, and resilient was the hard, and more rewarding eventually.
There's plenty left to do: per-employer handlers for the common non-standard portals, broader job sourcing for non-technical roles, tailored-resume generation and rendering, deeper extension coverage of custom widgets, and generalizing the features beyond their technical roots. But the core thesis holds, an AI agent can meaningfully reduce the application grind, provided it's honest about its limits and built to degrade gracefully when it reaches them.
Built with Next.js, Flutter, a Chrome extension, Clerk, Supabase, Drizzle, and Playwright, with language models handling resume parsing, field resolution, and fit analysis. Thanks for reading.