Upload a resume once. Get back only the jobs actually worth applying to.
A full-stack app that searches jobs across a dozen sources, scores every posting against your actual resume (not just the job title), and shows you only the ones that clear the bar — with a transparent breakdown of exactly why each one matched.
Warning
Auto Apply does not work yet. The UI for it exists and the plumbing is wired up, but the actual "submit applications on your behalf" part is unimplemented for every portal except a best-effort, unverified LinkedIn Easy Apply flow. Job search and matching are the parts that work — see What's Working below.
- What it does
- What's Working
- Matching, explained
- Architecture
- Supported job portals
- Quick start
- Configuration
- Project structure
- Testing
- Roadmap
- Contributing
- Disclaimer
- License
- Upload a resume (PDF/DOCX) → parsed once into a structured, reusable profile (skills, titles, experience, education) and embedded, so it's never re-processed per job.
- Tell it what you want: job role, one or more locations (Remote included), minimum salary. No maximum salary field — that's not a real constraint anyone searches by.
- It searches up to 12 job sources in parallel, normalizes every result into one schema, and deduplicates postings that show up on more than one portal.
- Every remaining posting is scored 0–100 against the actual job description (not just the title) — required skills, preferred skills, seniority/experience fit, domain overlap, semantic similarity via local sentence embeddings. Domain-agnostic: works the same for a software engineer resume as an instrumentation engineer's.
- Only postings at or above a configurable threshold survive, each with a plain-language breakdown: ✅ matching skills, 〜 partial matches, ❌ what's missing.
- Export the qualified list to Excel, ranked by match score, relevance, salary, and recency.
flowchart TD
A[Resume Upload] --> B[Parse Once: Structured Profile + Embeddings]
B --> C[Search Job Portals]
C --> D[Normalize to Common Schema]
D --> E[Deduplicate Across Portals]
E --> F[Cheap Filters: Role, Location, Salary]
F --> G[Fetch Missing Job Descriptions]
G --> H[Batch Semantic Matching]
H --> I{Score at or above threshold?}
I -- No --> J[Discard]
I -- Yes --> K[Rank: Score, Relevance, Salary, Date]
K --> L[Display Results + Export to Excel]
The expensive step — semantic matching — only ever runs on the small pool that survives the cheap role/location/salary filters, so a search that finds 1,000+ raw listings still finishes in seconds, not minutes.
| Capability | Status |
|---|---|
| Resume parsing (PDF/DOCX → structured profile) | ✅ Working |
| Job search across multiple portals in parallel | ✅ Working |
| Cross-portal deduplication | ✅ Working |
| Resume ↔ job description matching + explainability | ✅ Working |
| Configurable score threshold, ranking | ✅ Working |
| Excel export of qualified jobs | ✅ Working |
| Auto Apply (automated form submission) | 🔴 Not working yet |
Portals that work out of the box, no setup beyond cloning: Freehire, RemoteOK, Arbeitnow. Working with a free API key: Adzuna, Jooble. Working with extra setup: LinkedIn (needs an external CLI, see LinkedIn search). Full per-portal status — including the ones that don't work and why — is in Supported job portals.
Auto Apply has a complete UI and real backend plumbing (batching, status tracking, OTP/CAPTCHA handling), but actual form submission is only attempted for LinkedIn Easy Apply, and that path is unverified against the live site. Every other portal is marked "not supported" rather than silently doing nothing. Don't rely on it yet — job search and matching are what this project actually delivers today.
Every score comes with a receipt — the UI shows exactly what drove it:
Software Engineer 91%
ABC Technologies · Mumbai · ₹10–15 LPA
✅ Strong Matches Python · FastAPI · PostgreSQL · AWS
〜 Partial Docker · CI/CD
❌ Missing Kubernetes
The scoring model combines:
- Skill extraction that isn't tied to a fixed vocabulary — it reads whatever the candidate labeled under their own "Skills" section and cross-checks against a broad tech-term dictionary, so it works equally well outside software roles.
- Semantic similarity (local
sentence-transformers, no API calls) between the resume's facets (titles, skills, domains, responsibilities, education) and the posting text. - Experience alignment that's forgiving of small gaps (a posting asking for a couple more years than you have isn't auto-rejected — real hiring gives near-misses a shot) but honestly penalizes large ones.
- An optional LLM refinement pass (if
OPENAI_API_KEYis set) for borderline cases only — the expensive step never runs on the whole pool.
flowchart LR
FE[React + TypeScript<br/>Frontend]
BE[FastAPI Backend]
JS[Job Search Engine<br/>12 portal adapters]
ME[Matching Engine<br/>local embeddings + skill extraction]
DB[(SQLite)]
XL[Excel Export / Import]
FE -- REST API --> BE
BE --> JS
JS --> ME
ME --> DB
DB --> BE
BE --> XL
BE -- JSON --> FE
The existing frontend and backend stay the frontend and backend — job search / matching is a capability the backend calls into, not a separate app bolted on top.
| Portal | Method | API key needed | Status |
|---|---|---|---|
| Freehire | Public JSON API | No | ✅ Working |
| RemoteOK | Public JSON API | No | ✅ Working |
| Arbeitnow | Public JSON API | No | ✅ Working (light rate limits) |
| Adzuna | Official API | Yes (free) | ✅ Working |
| Jooble | Official API | Yes (free) | ✅ Working |
| External CLI (public guest endpoints) | No, but needs the CLI | 🟡 Needs setup, see below | |
| Glassdoor | HTML scraping | No | 🟡 Unverified |
| JSearch (RapidAPI) | Official API | Yes (free tier) | 🟡 Plan-dependent |
| Naukri | HTML/API scraping | No | 🔴 Bot-blocked |
| Indeed | RSS + HTML scraping | No | 🔴 Bot-blocked |
| Shine | HTML scraping | No | 🔴 Site broken |
| TimesJobs | HTML scraping | No | 🔴 Site broken |
Adding a new portal means writing one adapter with an async search(query, location, **kwargs)
method returning a list of dicts — see any file in backend/app/job_search/ as a template, then
register it in backend/app/job_search/__init__.py.
LinkedIn results come from a small external CLI that talks to LinkedIn's public jobs-guest
endpoints (no login, zero runtime dependencies, just bun + fetch). It is not bundled with
this repo. Its own README is explicit that this is for personal use only — LinkedIn's Terms
of Service prohibit automated access, so keep volume low and don't use it commercially.
If you have (or build) a compatible CLI, point LINKEDIN_CLI_PATH in backend/.env at it and
LinkedIn results will start flowing in. If you don't, nothing breaks — the search simply logs a
warning for that portal and continues with everything else.
- Python 3.10+
- Node.js 18+
- (Optional)
bun— only needed for LinkedIn search
cd backend
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
playwright install chromium # only needed for the (experimental) auto-apply path
cp .env.example .env # then fill in whatever keys you want to use
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000cd frontend
npm install
npm run devOpen http://localhost:5173. Every API key in .env is optional — the app runs and returns
real results with zero keys configured (Freehire + RemoteOK + Arbeitnow, plus LinkedIn if you've
set up the CLI).
docker compose up --buildFull list lives in backend/.env.example. The ones worth knowing about:
| Variable | Default | What it does |
|---|---|---|
MATCH_THRESHOLD_PERCENT |
50 |
The one place the qualification cutoff lives — never hardcoded elsewhere. |
SALARY_UNDISCLOSED_POLICY |
include |
include keeps salary-undisclosed jobs (labeled "Not Disclosed"); exclude drops them. |
LLM_REFINEMENT_MARGIN |
10 |
Borderline jobs within this many points of the threshold get an optional LLM second opinion (needs OPENAI_API_KEY). |
LINKEDIN_CLI_PATH |
— | Path to the external LinkedIn CLI. See LinkedIn search. |
JSEARCH_API_KEY / JOOBLE_API_KEY / ADZUNA_APP_ID + ADZUNA_APP_KEY |
— | Free-tier API keys. Portals without a key are simply skipped, not errored. |
OPENAI_API_KEY |
— | Upgrades resume parsing from regex/heuristics to LLM extraction, and enables borderline-match refinement. |
job-automation/
├── frontend/ # React + TypeScript + Vite + Tailwind
│ └── src/
│ ├── pages/ # FindJobs, AutoApply
│ ├── components/ # JobCard, MatchScore, PortalBadge, ...
│ ├── services/ # API client
│ └── store/ # Zustand state
├── backend/ # FastAPI + Python
│ └── app/
│ ├── api/ # REST endpoints
│ ├── job_search/ # One adapter per portal
│ ├── matching/ # Scoring / explanation engine
│ ├── services/ # Pipeline stages: dedup, filters, excel, resume parsing
│ ├── auto_apply/ # LinkedIn Easy Apply automation (unverified)
│ ├── models/ # SQLAlchemy models
│ ├── schemas/ # Pydantic schemas
│ └── security/ # Credential encryption (Fernet)
└── docker-compose.yml
cd backend
pytestCovers deduplication, cheap filters, salary parsing, and the matching/scoring logic.
- Verify (and likely fix) the LinkedIn Easy Apply flow against the live site
- Implement auto-apply for at least one more portal, or be explicit that it stays LinkedIn-only
- Resolve the JSearch
/search404 (pending RapidAPI plan verification) - Live-verify the Glassdoor adapter
- Package the LinkedIn CLI as an installable dependency instead of a manual path
Ways to help
- Fix a broken portal adapter — Naukri/Indeed are actively bot-blocked and out of scope (see Disclaimer), but Shine/TimesJobs/JSearch/Glassdoor are plausibly fixable.
- Verify or fix the auto-apply flow against a real LinkedIn account.
- Add a new portal adapter — one async
search()method, see Supported job portals. - Improve matching quality —
backend/app/matching/matching_service.pyis the whole scoring model in one file.
Open a PR. Run pytest in backend/ first.
This project scrapes and queries third-party job portals. Several of those portals' Terms of Service restrict or prohibit automated access — this project does not attempt to bypass CAPTCHAs, bot-detection, or other access controls, and adapters that hit such walls (Naukri, Indeed) are documented as blocked rather than worked around. Use every portal integration, especially LinkedIn search and the auto-apply feature, at your own risk and in accordance with that portal's terms. Not affiliated with LinkedIn, Naukri, Indeed, Glassdoor, Shine, TimesJobs, or any other listed portal.