Personal job-recommendation desktop app. Pulls Software Engineer postings from five sources (Adzuna, The Muse, RemoteOK, Findwork, Workable), parses your resume PDF, and ranks jobs by hybrid semantic + keyword similarity — no paid AI key required.
Features:
- Local Scoring: Uses a small (~25 MB) ONNX model running locally to score jobs. No data leaves your machine for matching.
- Manual Entries: Add and track jobs you found elsewhere (LinkedIn, etc.).
- Application Tracking: Mark jobs as applied, add personal notes, and keep track of your job search progress.
- Term Boosting: Boost specific keywords (like "Rust" or "Security") to influence matching scores.
- Privacy First: All data (resume, jobs, settings) stays in a local SQLite database.
You can visit https://publicapis.io/category/jobs to see what job APIS that are available.
- Desktop shell: Electron 35 (ESM main process, ships Node 22)
- Backend: Node.js + Express + TypeScript,
better-sqlite3for local storage,pdf-parsefor resume parsing,naturalfor TF-IDF scoring,node-cronfor scheduled refresh. In the desktop app this runs in-process inside Electron's main process, bound to a random port on127.0.0.1. - Frontend: Angular 18 (standalone components, signals).
jobdash/
├── electron/ # Electron main process (loads backend in-process, opens BrowserWindow)
├── backend/ # API server, scheduler, matching engine
├── frontend/ # Angular dashboard
└── package.json # Top-level: electron + electron-builder + orchestration scripts
Node is installed via mise (mise use -g node@22).
# from repo root — installs root deps, then both subprojects
npm install
npm --prefix backend install
npm --prefix frontend installThe first npm install runs electron-builder install-app-deps, which rebuilds better-sqlite3 against Electron's Node ABI. The backend/ postinstall does the same automatically whenever you reinstall backend deps, so you generally don't need to think about native rebuilds.
If you ever see NODE_MODULE_VERSION errors at launch, run:
npx electron-rebuild -f -w better-sqlite3 -m backendnpm run build # builds backend, frontend (with relative base href), and electron main
npm start # launches Electron, loads built bundle# terminal 1 — Angular dev server on :4200
npm run dev:frontend
# terminal 2 — Electron pointed at the dev server
npm run dev:electrondev:electron sets JOBDASH_DEV=1, builds the backend + main process, then opens DevTools on launch.
npm run dist # runs `npm run build` then electron-builderOutputs to dist/ (or release/) — .dmg on macOS, .exe (NSIS) on Windows, AppImage on Linux. Configure appId, icon, and signing in the build block of root package.json.
The Express server still works on its own:
cd backend
npm run dev # http://localhost:3001In standalone mode the frontend can be served by ng serve separately:
cd frontend
npm start # http://localhost:4200Two ways to provide keys, in order of precedence:
- In-app Settings (recommended for the desktop app) — click ▸ API keys in the dashboard, paste your keys, Save. They're stored in the local SQLite DB at
~/Library/Application Support/jobdash/jobdash.db(macOS) and never displayed again — only a "set / missing" status badge. - Environment variables (fallback, also used by the standalone backend in dev) — see
backend/.env.example.
Adzuna — register at https://developer.adzuna.com/signup. Free account; copy app_id and app_key from your dashboard. Env vars: ADZUNA_APP_ID, ADZUNA_APP_KEY, ADZUNA_COUNTRY (default us).
Findwork — register at https://findwork.dev/register. After email confirmation, find your token at https://findwork.dev/dashboard/api-token. Env var: FINDWORK_API_KEY.
The Muse, RemoteOK, Workable — no key required; fetched anonymously.
DEFAULT_SEARCH_TITLE(defaultSoftware Engineer)DEFAULT_SEARCH_LOCATION(defaultPortland, Oregon)DEFAULT_INCLUDE_REMOTE(defaulttrue)REFRESH_CRON(default0 6 * * *— 6am daily)DATABASE_PATH— overridden by Electron at startup to point at the per-user data dir; defaults to./data/jobdash.dbfor the standalone backend.
- Launch the app (
npm start). - Open the API keys panel and add your Adzuna + Findwork credentials (or set env vars before launch).
- Upload your resume PDF (top panel).
- Click Refresh now — fetches from all five sources and scores every job against your resume.
- Scores are 0–100%. Sort is by score DESC. Use the min-score slider to filter noise.
- Filters: source chips (click to toggle), US-only, Posted within 24h / 7d / 30d, Saved-only, Show hidden, Applied-only.
- Click a job to expand the description. Save (★), hide (✕), or mark as applied (check icon). Edit job details or add personal notes.
- While the app is open, the cron triggers a refresh at 6am local. (Closed apps don't fetch — there's no background daemon.)
- Manual Add: Click the "+" button to manually add jobs you found elsewhere to keep all your applications in one place.
Hybrid scoring blends two signals: 70% semantic embedding similarity + 30% TF-IDF cosine + overlap bonus.
- PDF text → tokenized, stopwords removed.
- Embedding half: resume and each job (
title + description) are encoded withXenova/all-MiniLM-L6-v2(quantized ONNX, ~25 MB). Cosine similarity is the dot product of the L2-normalised vectors. - TF-IDF half: TF-IDF over
[resume, job1, job2, ...]cosine similarity, plus a small bonus for overlap with the top-60 resume terms. Matched terms are surfaced in the UI as chips. - Final score:
0.7 × embedding + 0.3 × tfidf, clamped to[0, 1]. - Rescoring runs automatically on resume upload.
First refresh after install triggers a one-time download of the scoring model (~25 MB) into
~/.cache/huggingface/. The UI shows a download progress message during this. All subsequent refreshes use the cached model and are offline-capable for the scoring step.
GET /api/jobs?showHidden=&savedOnly=&minScore=POST /api/jobs/refreshPATCH /api/jobs/:idbody{ hidden?, saved?, applied?, notes?, title?, company?, ... }POST /api/jobs/manualbody{ title, company, ... }GET /api/jobs/statsGET /api/jobs/sourcesGET /api/resume·POST /api/resume(multipartresume, PDF)GET /api/settings·PUT /api/settingsbody{ title?, location?, includeRemote? }GET /api/settings/keys(returns booleans for secrets, never the values)PUT /api/settings/keysbody{ adzunaAppId?, adzunaAppKey?, adzunaCountry?, findworkApiKey? }(empty string clears)
The renderer reads its API base from ?apiPort=<n> injected into index.html at launch.
- Database location:
~/Library/Application Support/jobdash/jobdash.db(macOS) when running under Electron;backend/data/jobdash.dbfor the standalone backend. WAL mode. - Resume uploads are processed in memory; only the parsed text + filename are persisted to SQLite.
.envis gitignored. Rotate Adzuna keys if they leak.- LinkedIn is intentionally excluded — no public API, and scraping violates their ToS.
- After installing/updating backend deps standalone (
cd backend && npm install), the postinstall auto-rebuildsbetter-sqlite3against Electron's ABI when run inside this monorepo. - macOS builds are arm64 only (Apple Silicon). Intel Macs are not supported by the packaged DMG. The
onnxruntime-nodeand@huggingface/transformerspackages are unpacked from the asar archive at install time so their native binaries (.node,.dylib) can be loaded.