ApplyEase — Local, Privacy‑First Job Application Assistant
Overview
-
Auto‑fills common job application fields (first/last name, email, phone, links) and uploads your resume.
-
Computes a Resume ↔ Job Description match score with tech‑term highlights (matching/missing keywords).

-
Generates concise custom answers to application questions using a local LLM (no paid APIs).
-
NEW: Structured resume builder (summary, title, skills, multiple experiences, education) with templated tailored CV generation and PDF export.
-
NEW: Cover letters — generate LLM‑enhanced letters or clean templates; saved history with downloads.
-
NEW: Job Tracker — board and list view, drag‑and‑drop between stages (Saved → Applied → Interview → Offer → Rejected), quick cover‑letter button.
-
NEW: Extension auto‑tracking — captures an “Applied” job automatically on submit for many job sites.

-
Works via a Chrome extension with a small React dashboard and a FastAPI backend.

Architecture
- Backend: FastAPI (
applyease-backend/app.py) + PostgreSQL withpgvectorfor embeddings. SentenceTransformerall-MiniLM-L6-v2. Routers:routes/job_tracker.py,routes/cover_letters.py. Local LLM: Ollama (default) or LM Studio/OpenAI‑compatible. - Frontend: React app in
frontend/(login, dashboard, resume builder, cover letters tab, job tracker board/list). - Chrome Extension: Autofill, JD extraction, on‑page match widget, popup with keywords, job tracker button, auto‑tracking.
Prerequisites
- Python 3.9+
- PostgreSQL with
pgvectorextension available - Node.js 16+ and npm for the frontend
- Chrome (or Chromium‑based) for the extension
- Local LLM
- Ollama (recommended): https://ollama.ai — e.g.,
ollama pull llama3.1:8b - OR LM Studio / any OpenAI‑compatible local server
- Ollama (recommended): https://ollama.ai — e.g.,
Quick Start
-
Database
- Create DB
applyeaseand ensure pgvector is installed/enabled- psql:
CREATE DATABASE applyease; \c applyease; CREATE EXTENSION IF NOT EXISTS vector;
- psql:
- Create DB
-
Backend
cd applyease-backendpython3 -m venv .venv && source .venv/bin/activatepip install -r requirements.txt- Optional env (defaults in parentheses):
- DB:
PGHOST(localhost),PGPORT(5432),PGUSER(your OS user or postgres),PGDATABASE(applyease),PGPASSWORD(empty) - Auth:
JWT_KEY(dev-secret),JWT_EXPIRES_IN_MIN(60) - LLM:
LLM_PROVIDER(ollama|lmstudio),LLM_MODEL(e.g., llama3.1:8b),OLLAMA_HOST(http://localhost:11434),LLM_BASE_URL(http://localhost:1234/v1)
- DB:
- Start:
uvicorn app:app --reload --port 8000 - On first run, the service will create tables
users,resumesand try to create thevectorextension and an IVFFlat index.
-
Local LLM
- Ollama (recommended):
- Install Ollama and run:
ollama pull llama3.1:8b export LLM_PROVIDER=ollamaandexport LLM_MODEL=llama3.1:8b
- Install Ollama and run:
- LM Studio (or compatible):
- Start local server (usually
http://localhost:1234/v1) with your model export LLM_PROVIDER=lmstudio,export LLM_BASE_URL=http://localhost:1234/v1,export LLM_MODEL=<your-model-name>
- Start local server (usually
- Ollama (recommended):
-
Frontend
cd frontendnpm install- Optional:
export REACT_APP_API_BASE=http://localhost:8000 - Run:
npm start(dev server on http://localhost:3000)
-
Chrome Extension
- Go to chrome://extensions → Enable Developer Mode → Load unpacked → select this repo folder.
- The extension injects on all frames at document_end for better compatibility.
Basic Flow
- Sign up or login in the web app (http://localhost:3000). JWT is saved to localStorage and also broadcast to the extension.
- In the dashboard, upload your resume (PDF) and update profile fields. This embeds and stores the resume in Postgres.
- Visit a job posting (LinkedIn/Indeed/Workday/Greenhouse/Lever/etc.).
- A floating “Resume Match: XX%” widget appears on the page (computed from the JD on load).
- Click the widget to open the extension popup, which shows matching/missing keywords.
- Click “Auto Fill” in the popup to fill the form.
- Fills first/last name (or full name field), email, phone, location, URLs.
- Attaches your resume as a PDF from the backend (
/resume_pdf). - Adds “Fill” buttons next to textareas to generate tailored answers locally.
- Resume Builder and Tailoring
- Build your resume sections (title, summary, skills, experiences with bullets, education) and save.
- Paste a JD and click “Generate Tailored CV” to render a templated PDF that prioritizes relevant experiences.
- Click “Generate Cover Letter” to create a letter PDF using the JD and your profile/sections (uses local LLM if configured).
- Job Tracker
- Open Job Tracker from the dashboard or extension popup.
- Add jobs or drag cards between columns to update status; use List view if preferred.
- Auto‑tracking: on many sites, after you submit an application, the extension records the job with status “applied”.
Key Endpoints (Backend on :8000)
-
POST
/signup→{ token, user } -
POST
/login→{ token } -
GET
/user(Bearer) -
PATCH
/user(Bearer, multipart:resumefile + fieldsfirst_name,last_name,urlsJSON, etc.) -
GET
/resume(Bearer) →{ resume_text } -
GET
/resume_pdf(Bearer) → stream of generated PDF -
POST
/match(Bearer) →{ percent, matchingWords, missingWords, score } -
POST
/custom-answer(Bearer, local LLM only) →{ answer } -
POST
/upsert_resume(service) →{ ok, user_id } -
Structured resume sections
- GET
/resume_sections(Bearer) →{ summary, title, experiences, education, skills } - POST
/resume_sections(Bearer) → upserts sections and refreshes embedding/keywords
- GET
-
Tailored resume (templated)
- POST
/generate_tailored_resume(Bearer) →{ id, download_url }(PDF saved in history) - GET
/tailored_resumes/ GET/tailored_resume_download?id=...
- POST
-
Cover letters
- POST
/cover_letters/generate(Bearer) → PDF stream; body supports{ company, title, job_description, filename, save, use_llm } - GET
/cover_letters, GET/cover_letters/download?id=...
- POST
-
Job tracker
- GET
/jobs(Bearer) → array of jobs - POST
/jobs(Bearer) → create job{ company, title, ... } - PATCH
/jobs/{id}(Bearer) → partial update (e.g.,{ status: 'interview' }) - DELETE
/jobs/{id}(Bearer) - GET
/jobs/stats(Bearer)
- GET
Database Schema (auto‑created)
users(id text pk, first_name, last_name, email unique, password_hash, phone, location, urls jsonb, eeo jsonb, created_at, updated_at)resumes(user_id text pk, resume_text text, embedding vector(384), resume_keywords text[], updated_at)- Plus structured fields:
summary text, title text, experiences jsonb, education jsonb, skills text[], and optional stored resume file (blob/mime/filename)
- Plus structured fields:
tailored_resumes(id text pk, user_id, job_description, resume_text, resume_blob, resume_mime, resume_filename, created_at)cover_letters(id text pk, user_id, job_id, company, title, letter_text, letter_blob, letter_mime, filename, created_at)job_applications(id text pk, user_id, company, title, location, source, url, status, notes, jd_text, next_action_date, created_at, updated_at)
Smoke‑Test (curl)
- Signup:
curl -sS -X POST http://localhost:8000/signup -H 'Content-Type: application/json' -d '{"first_name":"Test","last_name":"User","email":"test@example.com","password":"pass123"}' - Upsert resume (text):
curl -sS -X POST http://localhost:8000/upsert_resume -H 'Content-Type: application/json' -d '{"user_id":"<USER_ID>","resume_text":"Software engineer skilled in Python, AWS, and Docker."}' - Health:
curl -sS http://localhost:8000/healthz - Match (Bearer):
curl -sS -X POST http://localhost:8000/match -H "Authorization: Bearer <TOKEN>" -H 'Content-Type: application/json' -d '{"jobDescription":"Looking for backend developer with AWS and Python."}' - Custom answer (Bearer, local LLM):
curl -sS -X POST http://localhost:8000/custom-answer -H "Authorization: Bearer <TOKEN>" -H 'Content-Type: application/json' -d '{"jobDescription":"...","applicationQuestion":"Describe a project that demonstrates your impact."}'
Troubleshooting
- Postgres/pgvector: If startup fails creating the
vectorextension, install pgvector on your DB and retry. Ensure envPG*vars point to the right DB. - Autocommit error: We set autocommit before using pgvector; ensure you’re on the latest
applyease-backend/app.py. - Local LLM:
- Ollama: ensure the model is pulled and
LLM_MODELis set (e.g.,llama3.1:8b). - LM Studio: verify base URL
/v1and the model name; set envs as above.
- Ollama: ensure the model is pulled and
- CORS: The backend enables permissive CORS so the web app and content scripts can call it. Restart the server after installing deps.
- Extension not autofilling:
- Confirm token: open DevTools on the job page and run
chrome.storage.local.get('token', console.log). - Some sites load forms in iframes; we inject into all frames (
all_frames: true). - Resume upload: Browsers block programmatic file pickers. We set
input.filesvia DataTransfer when allowed; otherwise we attempt drag‑and‑drop onto known dropzones. - Auto‑tracking: If a job isn’t captured, it likely didn’t expose title/company reliably; we can add site‑specific selectors upon request.
- Confirm token: open DevTools on the job page and run
- Matching seems low with all terms: The score is cosine similarity of embeddings, not a keyword count. Keywords are diagnostic only.
Notes
- Embeddings model:
all-MiniLM-L6-v2(384‑dim). Vectors stored normalized; cosine via dot product. - Keywords: tech‑focused extraction with curated allowlist + heuristics; generic job terms ignored.
- Data lives in Postgres; to reset, drop the
resumesanduserstables.
License
- For personal use. Do not upload sensitive information to third‑party sites without review.
