System Architecture, Plainly Explained

How Recruit Assistant Works

A recruiter's desk usually ends up with resumes scattered across a downloads folder and an overflowing inbox, and matching any of them to an open role means reading each one by hand. Recruit Assistant reads both places, figures out who's who even if someone applied twice, and uses AI to score and explain how well each person fits a job — entirely on your own computer, with nothing sent anywhere you haven't explicitly connected.

Runs on: your own laptop, not a shared server Data leaves your machine: only if you connect an email account Reading time: ~9 minutes
00 — At a Glance

The system, in one diagram

Three layers. You use a normal web page in your browser; that page talks to one program running on your computer; that program is the only thing that ever touches your data, your files, or the outside world.

You, in a browser The Recruit Assistant web page Jobs, candidates, matches, everything you click and read
asks for / sends data over a local connection
One program on your machine The Recruit Assistant backend Reads resumes, runs matching, makes every decision described on this page Why one program? Splitting into several small services that talk over a network is how a large, team-run platform scales — this runs on one laptop, so that complexity would buy nothing. The code inside is still split into clean modules, so any one piece could become its own service later without a rewrite.
and in turn reads from / talks to three kinds of things
Always, locally Your own files A local database + a folder of resumes on your disk — never leaves your machine
For scoring An AI provider OpenRouter first, OpenAI as a fallback — only the resume text needed for that one request is sent
Only if you connect one Your email inbox Gmail or Outlook, read-only — checked for resume attachments, nothing else Why keychain, not database? The login token for a connected inbox is handed to your OS's own secure credential store, not saved alongside everything else — so disconnecting an account wipes it completely, with nothing left behind.

Nothing in the bottom row talks to anything else in the bottom row — the backend is the only thing that ever coordinates between them.

01 — Walkthrough

The journey of one resume

Follow a single resume from the moment it lands to the moment it shows up as a color-coded match. Every stage below is a real step the software takes — nothing skipped, nothing simplified away. Steps tagged you decide are places a human makes the call; everything else runs automatically.

Phase 1 — getting a resume on file
1Arrives
2Gets read
3Gets understood
4Gets recognized
5Gets filed
Phase 2 — matching it to a job (only when you click "Run matching")
6You describe the job
7Field gets narrowed
8Shortlist gets scored
9Second opiniononly scores 40–70
10Results shown
You decide

It arrives — from a folder, or an inbox you've connected

You either point the app at a folder of resumes already on your computer, or let it check a mailbox you've connected (read-only — it can only look, never send or delete). From this point on, it makes no difference which door a resume came through; both are treated identically for everything downstream.

scanning/folder_ingestor.py scanning/email_ingestor.py — one shared output shape either way (IngestedResume)
Why

The simpler default for an integration like this is to request broad mailbox access "just in case." Instead, a connected mailbox is granted read-only permission from the start — the app is structurally incapable of sending or deleting anything, even if something went wrong.

Automatic

It gets read

The text is pulled out of the PDF, Word document, or plain text file. If it's a scanned image with no real text underneath it — a photocopy of a resume, say — the software falls back to optical character recognition (OCR), essentially "reading" the picture the same way a person would, rather than giving up.

scanning/parser.py — pdfplumber/pypdf/python-docx first, OCR (pytesseract) only if that comes back too thin
Automatic

It gets understood

An AI model reads the extracted text and pulls out the structured facts a recruiter actually cares about: legal name, contact info, skills, years of experience, education, work-authorization status. Anything it's not confident about is left blank rather than guessed — a blank field is a prompt to double-check, a wrong guess is a silent mistake.

scanning/parser.py — structured LLM extraction, cross-checked against pattern-matched email/phone/LinkedIn/GitHub
Automatic

It gets recognized

Before saving anything, the system asks: have we seen this person before? It checks by email address first, then by name-plus-phone as a fallback. If yes, this new resume updates their existing profile — new skills get added, a longer work history overwrites a shorter one — instead of creating a confusing second entry for the same human being.

scanning/identity_resolution.py — the reason someone who applies again six months later, with a stronger resume, stays one person, not two
Why

The easy path is to keep email-sourced and folder-sourced resumes as two separate lists, since they arrive through two different doors. That would defeat the point — a candidate is a person, not a source — so every resume is checked against everyone already on file before it's ever saved.

Automatic

It gets filed

The resume itself, a short AI-written summary, and a small metadata note all get saved to an actual folder on your disk, organized by date — so you can find and open the real file later, even offline, even years from now. The structured facts also get saved to a local database so the app can search and filter across everyone.

scanning/mirror_writer.py data/candidates/<date>/<name>/resume.pdf + summary + a local SQLite database
Why

Starting with a cloud database is the modern default for most software — but that would mean your candidates' data lives on someone else's server from day one. Instead, storage sits behind one clean interface: local now, with a door deliberately left open for a company server or cloud backend later, as a plug-in, not a rewrite.

You decide

You describe the job, and ask for matches

You write or paste a job description and click "Run matching." Behind the scenes, this is where things get interesting — the next three steps only happen when you ask for them, and they're what actually turn a pile of resumes into a ranked shortlist.

Automatic

The field gets narrowed — cheaply, first

Reading every single resume in depth with AI, for every job, would be slow and expensive at real volume. So a fast, inexpensive pass compares the meaning of the job description against everyone's profile using something called an embedding — a mathematical fingerprint of what a piece of text is actually about — and keeps only the strongest handful of candidates for the next, more careful step.

matching/embeddings.py — cosine similarity over the whole pool, computed once per candidate and reused, not recalculated every time
Why

The most thorough-sounding approach is to have the AI carefully read and score every resume in the pool. At real volume that's slow and expensive for no real benefit — most candidates were never going to be close matches. Match cheaply first, think carefully second keeps both wait time and AI cost bounded, without losing quality where it actually matters.

Automatic

The shortlist gets scored, with reasons

Only the narrowed-down shortlist gets the expensive, careful treatment: a stronger AI model reads the actual job description against the actual resume and produces a 0-100 score, a plain-language list of what matched, what's missing, and any information that's still unclear — never just a bare number with no explanation.

matching/matcher.py deep_score(), run concurrently across the shortlist rather than one at a time
Automatic

Borderline calls get a second opinion

Any score that lands in an ambiguous middle range (not clearly a great fit, not clearly a poor one) gets reviewed a second time by an independent AI pass acting as a judge — it can agree with the original score, or correct it. This is the "AI checking AI" step, aimed at exactly the calls a human reviewer would also want to look at twice.

matching/matcher.py judge_score(), triggered only for scores between 40 and 70
Why

A single AI opinion can be confidently wrong, especially on close calls. Rather than trust one score all the way through, the same ambiguous middle range a human reviewer would double-check gets an independent second AI opinion too — cheap insurance, spent only where it's actually needed.

You decide

Results show up, color-coded — and you take it from there

Great, Good, Average, or Poor — plus a red-flag override for anything that should never be buried in the middle of a list regardless of score. Every result carries its reasoning, so you're reviewing a case, not trusting a black box. From here you can flag a candidate, or generate a draft outreach email pre-filled with the actual reasons this person is a good fit.

matching/matcher.py (score_to_tier) email_draft/generator.py
02 — Components

The building blocks

Six pieces do essentially all of the work. Each one solves exactly one problem, and none of them know or care about the internal details of the others — which is what lets any one of them change later without the rest breaking.

Intake

Two front doors, one filing system

Folder scanning and email scanning are built as two interchangeable "readers" that both hand off the same shape of information to everything downstream. Adding a third source later — a LinkedIn export, an applicant-tracking-system integration — means writing one more reader, not touching anything else.

scanning/ingestor_base.py — one shared interface, several implementations
Intelligence

The matching engine

The cheap-filter → deep-score → judge-review pipeline described above, all in one place. It's deliberately staged rather than one giant AI call, so cost and speed stay predictable even with a pool of tens of thousands of candidates.

matching/ — embeddings, matcher, judge, prompts, in one module
Memory

Storage that's local today, portable tomorrow

Everything lives in a local database and a local folder of files — no server, no account, no subscription required to keep using your own data. It's built behind one swappable interface, so a future move to a shared company server or the cloud is a new implementation of that interface, not a rewrite of the app.

storage/base.pystorage/local.py (SQLite + files)
Judgment

Criteria you actually control

Beyond the AI's own read of a resume, you can turn on specific weighted criteria per job — years of experience, location, visa eligibility, certifications — and re-run matching against them without re-scanning every source from scratch.

criteria/builtin.py, criteria/service.py
Automation

An optional nightly assistant

You can mark specific folders or mailboxes to be re-scanned automatically overnight — but nothing runs on a schedule unless you explicitly turn it on, source by source. No background behavior is sprung on you.

scheduler/ — off by default (SCHEDULER_ENABLED=false)
Output

From match to message

A strong match doesn't just sit in a list — one click drafts a personalized outreach email, pre-filled using the actual reasons the match scored the way it did, ready for you to edit and send yourself.

email_draft/generator.py
03 — Trust

Privacy, in plain terms

This handles real people's names, contact details, and work history, so the safeguards below aren't an afterthought — they're load-bearing.

Nothing leaves your machine by default

Candidate data and resumes stay in a local database and local files, unless you explicitly connect an email account or an LLM provider.

Passwords are never stored as plain text

Your login password is run through a one-way hashing process (PBKDF2) before it's saved — the app itself never keeps a readable copy.

Read-only mailbox access, always

A connected email account can only be scanned for attachments — never sent from, deleted from, or otherwise changed.

Login tokens live in your OS's own vault

Not in a database file, not in a config file — in the same secure credential store your operating system already uses for other saved passwords.

Nothing runs on a schedule unless you say so

Automatic overnight re-scanning exists, but is switched off by default and only ever touches sources you've explicitly opted in, one at a time.

You can offer up a test dataset instead

A built-in generator can create thousands of realistic fake resumes for trying the app out, so you never have to test with real candidates' data.

Deleting a candidate actually deletes them

A per-candidate delete removes their database record and every resume file on disk — not a soft "hide," a real, honest erasure for when someone asks to be forgotten.

Real AI scoring asks first, once

The first time you switch on real (non-mock) AI scoring, a plain-language dialog explains exactly what leaves the machine before you confirm — not buried in a settings toggle.

Only reachable from this machine

The server listens on localhost only, not the whole network — someone on the same office Wi-Fi can't reach it, and repeated failed logins lock out automatically.

04 — Reference

Under the hood

For the more technically curious — the actual tools this is built with, and how the code is laid out.

Technology

Python · FastAPI SQLAlchemy + SQLite React + TypeScript Vite Zustand Tailwind CSS OpenRouter (primary LLM gateway) OpenAI (fallback) Gmail API Microsoft Graph OS Keychain (keyring) APScheduler pytesseract (OCR, optional) Docker Compose

Where things live

# the shape of the codebase — modules map directly to the building blocks above backend/app/ ├── auth/ # single-account login, session tokens, rate limiting ├── scanning/ # folder + email readers, text extraction, identity matching ├── matching/ # embeddings, two-stage scorer, AI judge ├── storage/ # the swappable local-database interface ├── criteria/ # built-in + custom weighted matching criteria ├── email_auth/ # OAuth + OS keychain token handling ├── email_draft/ # outreach email generator ├── dashboard/ # one aggregated summary behind the dashboard ├── scheduler/ # opt-in nightly auto-scan ├── maintenance/ # backfill tasks when a feature ships after data exists └── routes/ # the API surface the frontend talks to frontend/src/ ├── pages/ # one screen per sidebar item ├── stores/ # client-side state, one per feature area └── components/ # the visual building blocks pages are made of
05 — Status

What's done, what's next

Shipped

The full pipeline above — folder + email intake, identity resolution, two-stage matching with judge review, criteria, outreach drafting, and a personal dashboard.

Shipped

Performance hardening — matching and scanning run concurrently instead of one item at a time, candidate search and similarity ranking stay fast at real volume (10,000+ candidates).

Shipped

Real-inbox readiness — connected accounts show their real address and refresh their own access automatically instead of quietly expiring.

Shipped

OCR fallback and cleaner multi-attachment handling — scanned resumes still get read, and a resume-plus-cover-letter email no longer confuses the two.

Shipped

Opt-in nightly scanning, Docker packaging, and a public repository with continuous testing on every change.

Shipped

Security hardening — the server only listens on this machine by default, not the whole network, and repeated failed logins lock out automatically. Followed a four-perspective stakeholder review that read the app as a recruitment analyst, a product owner, a data-governance lead, and a CEO, each independently.

Shipped

Pipeline stage tracking — where a candidate actually stands in the hiring process (sourced through placed or declined), tracked separately from match quality, on Match Results, Candidate Detail, and the dashboard.

Shipped

Real per-candidate deletion — an honest right-to-erasure path, distinct from the existing "clear everything" reset, that removes one person's database record and every file on disk.

Shipped

A one-time consent step before real AI scoring — asked once, remembered for this installation, explaining plainly what leaves the machine.

Later

Faster scans and matches — the mailbox-scanning and AI-matching pipelines are fully mapped for speed (parallelizing per-resume processing, smarter use of existing AI concurrency, shrinking what gets sent per scoring call) but not yet built.

Later

A locally-hosted, recruiting-specific AI model — long-term: a small open-source model, fine-tuned specifically for resume/job scoring and run entirely on the recruiter's own machine, to cut real-mode AI cost toward zero rather than paying a cloud provider per scoring call indefinitely. Not yet scoped — needs a training-data plan, a model-size-vs-laptop-hardware decision, and a quality bar to clear before it could replace today's cloud models as the default.

Later

A genuinely shared/cloud version — the storage interface is already built to support this; it isn't built out yet because nothing has needed it.

Later

Reading team culture/energy fit from email tone — deliberately deferred; a more sensitive feature that deserves its own careful design pass.