A personal job search aggregator that queries Indeed, Greenhouse, Lever, and LinkedIn simultaneously, deduplicates and scores results, and provides a full application-tracking pipeline — all in a single self-hosted app.
- Search across Indeed, Greenhouse, Lever, and LinkedIn in one request
- Job title autocomplete with 50+ curated role suggestions
- Filters: location, remote-only, salary range, date posted, experience level, job type, sources
- Deduplication by (title, company) and relevance scoring with click-to-expand breakdown
- Save, Dismiss, or Track individual results; email any listing with one click
- Stale listing indicator (posted > 30 days ago)
- Dedicated tab showing all saved listings
- Bulk email all saved jobs to any address (Resend API)
- Dedicated tab showing all dismissed listings
- Restore dismissed jobs by saving or unsaving them
- Pipeline table for every job you're pursuing
- Columns: Job Title, Company, Location, Status, Date Added, Follow-up Date, Tags, Notes
- Status stages: Found → Reviewing → Applied → Interviewing → Offer → Rejected (colored badge, editable inline)
- Click-to-edit Notes and Follow-up Date per row; overdue dates highlighted red
- Drag-and-drop row reordering
- Quick-add modal for manually entered jobs
- "Save to Tracker" button on every search result card
- CSV export
- Save any search with a name and per-search notification email
- Re-run saved searches manually or on a schedule
- Scheduled digest emails (daily or weekly) via configurable send time
- Configurable email digest: recipient, frequency (off/daily/weekly), send time
- SMTP configuration for self-hosted email (falls back to Resend API)
- SQLite backend — all jobs, statuses, tracker entries, and settings survive restarts
| Layer | Tech |
|---|---|
| Backend | Python 3.11+ / FastAPI |
| Database | SQLite via aiosqlite |
| HTTP client | httpx (async) + curl_cffi |
| Scheduling | APScheduler |
| Resend API (or SMTP) | |
| Frontend | React 18 / Vite |
| Routing | React Router v6 |
JobJames/
├── backend/
│ └── app/
│ ├── main.py # FastAPI app + CORS + lifespan
│ ├── config.py # Settings (pydantic-settings, .env)
│ ├── database.py # SQLite schema + async CRUD helpers
│ ├── api/
│ │ ├── jobs.py # Search, get, status endpoints
│ │ ├── email_route.py # Bulk + per-job email via Resend
│ │ ├── tracker.py # Tracker CRUD + reorder endpoints
│ │ ├── settings.py # App settings endpoints
│ │ ├── saved_searches.py# Saved searches CRUD + run
│ │ └── router.py # Mounts all routers
│ ├── crawlers/
│ │ ├── indeed.py # curl_cffi + mosaic JSON extraction
│ │ ├── linkedin.py # Guest jobs API
│ │ ├── greenhouse.py # Public board API
│ │ └── lever.py # Public posting API
│ ├── models/
│ │ ├── job.py # JobListing, JobStatus, JobSource
│ │ ├── tracker.py # TrackerEntry, TrackerStatus
│ │ └── search.py # SearchRequest
│ └── services/
│ ├── deduplication.py
│ ├── scoring.py
│ └── scheduler.py # APScheduler digest job
└── frontend/
└── src/
├── api/client.js # Fetch wrappers for all endpoints
├── components/
│ ├── JobCard # Card with Save/Dismiss/Track/Email actions
│ ├── SearchForm # Search form with autocomplete
│ └── StatusBadge # Colored status pill
└── pages/
├── HomePage # Search + results
├── SavedPage # Saved listings + bulk email
├── DismissedPage # Dismissed listings + restore
├── TrackerPage # Application pipeline table
└── SettingsPage # App settings + saved searches
cd backend
python -m venv .venv
# Windows
.venv\Scripts\activate
# macOS / Linux
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # add RESEND_API_KEY if you want email
uvicorn app.main:app --reload
# API at http://localhost:8000
# Swagger UI at http://localhost:8000/docscd frontend
npm install
npm run dev
# UI at http://localhost:5173Vite proxies /api/* to localhost:8000 so no CORS configuration is needed during development.
| Variable | Required | Description |
|---|---|---|
RESEND_API_KEY |
No | Enables email features (bulk + per-job) |
EMAIL_FROM |
No | Sender address for Resend (default: onboarding@resend.dev) |
GREENHOUSE_COMPANIES |
No | Comma-separated company tokens for Greenhouse crawler |
LEVER_COMPANIES |
No | Comma-separated company slugs for Lever crawler |
CORS_ORIGINS |
No | Allowed origins (default: http://localhost:5173) |
JobJames runs as two Railway services — one for the FastAPI backend and one for the React frontend.
- A Railway account
- Your repo pushed to GitHub
1. Create a new Railway project
Go to railway.app/new → "Deploy from GitHub repo" → select your JobJames fork.
2. Add the Backend service
- Click + New Service → GitHub Repo → select
JobJames - Set the Root Directory to
backend/ - Railway detects
railway.tomland uses Nixpacks withpip install -r requirements.txt
Add these environment variables in the backend service settings:
| Variable | Value |
|---|---|
DATABASE_URL |
/data/jobjames.db (with volume) or jobjames.db (ephemeral) |
ALLOWED_ORIGINS |
["https://your-frontend.up.railway.app"] |
RESEND_API_KEY |
Your Resend key (for email features) |
EMAIL_FROM |
JobJames <noreply@yourdomain.com> |
LINKEDIN_EMAIL |
(optional) |
LINKEDIN_PASSWORD |
(optional) |
GREENHOUSE_COMPANIES |
(optional) comma-separated tokens |
LEVER_COMPANIES |
(optional) comma-separated slugs |
Database persistence: SQLite is ephemeral on Railway by default — data is lost on redeploy. To persist data, add a Volume in Railway: mount path
/data, then setDATABASE_URL=/data/jobjames.db. A future Phase 5 migration will replace aiosqlite with SQLAlchemy + PostgreSQL for fully managed persistence.
3. Add the Frontend service
- Click + New Service → GitHub Repo → select
JobJames - Set the Root Directory to
frontend/ - Railway uses the
Dockerfile(multi-stage Node → nginx build)
Add this environment variable (required — baked into the build):
| Variable | Value |
|---|---|
VITE_API_URL |
The backend Railway URL, e.g. https://jobjames-backend.up.railway.app |
4. Generate a domain for the frontend
In the frontend service → Settings → Networking → Generate Domain.
Copy the URL (e.g. https://jobjames-frontend.up.railway.app).
5. Update backend CORS
Back in the backend service, update ALLOWED_ORIGINS to include the frontend domain:
ALLOWED_ORIGINS=["https://jobjames-frontend.up.railway.app"]
6. Deploy
Both services deploy automatically on git push. Monitor logs in the Railway dashboard.
Test the production Docker images locally before deploying:
# Copy and fill in backend env vars
cp backend/.env.example backend/.env
# Build and start both services
docker-compose up --build
# Frontend at http://localhost | Backend API at http://localhost:8000/docs| Method | Path | Description |
|---|---|---|
POST |
/api/jobs/search |
Run all crawlers and return ranked results |
GET |
/api/jobs/saved |
List all saved jobs |
GET |
/api/jobs/dismissed |
List all dismissed jobs |
GET |
/api/jobs/{id} |
Fetch a single listing |
PATCH |
/api/jobs/{id}/status |
Set status: new / saved / dismissed |
POST |
/api/jobs/{id}/email |
Email a single job listing |
POST |
/api/jobs/email |
Email all saved jobs (bulk) |
| Method | Path | Description |
|---|---|---|
GET |
/api/tracker |
List all tracker entries |
POST |
/api/tracker |
Manually add a job to the tracker |
POST |
/api/tracker/from-job/{job_id} |
Add a search result to the tracker |
PATCH |
/api/tracker/{id} |
Update status, follow-up date, or notes |
PATCH |
/api/tracker/reorder |
Persist drag-and-drop row order |
DELETE |
/api/tracker/{id} |
Remove an entry |
| Method | Path | Description |
|---|---|---|
GET |
/api/settings |
Get all app settings |
PUT |
/api/settings |
Save app settings |
| Method | Path | Description |
|---|---|---|
GET |
/api/searches |
List saved searches |
POST |
/api/searches |
Create a saved search |
PATCH |
/api/searches/{id} |
Update a saved search |
DELETE |
/api/searches/{id} |
Delete a saved search |
POST |
/api/searches/{id}/run |
Manually run a saved search |
{
"title": "Software Engineer",
"location": "New York, NY",
"remote": false,
"salary_min": 100000,
"salary_max": 180000,
"sources": ["indeed", "greenhouse", "lever", "linkedin"],
"date_posted": "week",
"experience_level": "senior",
"job_type": "fulltime"
}| Source | Status | Notes |
|---|---|---|
| Indeed | Working | curl_cffi with browser impersonation + mosaic JSON extraction |
| Working | Guest jobs API (unauthenticated) | |
| Greenhouse | Working | Public board API — populate GREENHOUSE_COMPANIES in .env |
| Lever | Working | Public posting API — populate LEVER_COMPANIES in .env |
Branch: main (PRs #1–5)
- Project scaffold (FastAPI + React + Vite)
- Search form with filters (title, location, remote, salary, sources, date, level, type)
- Job title autocomplete (50+ curated roles)
- Indeed crawler (curl_cffi + mosaic JSON)
- LinkedIn crawler (guest API)
- Greenhouse + Lever crawlers (public APIs)
- Deduplication by (title, company)
- Relevance scoring and ranking with click-to-expand breakdown
- Dashboard UI with ranked results
- Save / Dismiss status tagging per listing
- SQLite persistence
- Saved Jobs tab
- Email export via Resend (bulk + per-job)
- Loading indicator during search
Branch: phase-2-tracker (PR #7)
- Tracker tab with full pipeline table
- Status stages: Found → Reviewing → Applied → Interviewing → Offer → Rejected
- Inline-editable status, notes, and follow-up date
- Overdue follow-up date indicator
- Quick-add modal for manual entries
- "Save to Tracker" from search results
- CSV export
Branch: phase-3-power-tools (PR #9)
- Settings page (email digest: recipient, frequency, send time)
- SMTP configuration option (falls back to Resend)
- APScheduler integration for automated digests
- Saved Searches (save, name, re-run, per-search notification email)
- Tracker drag-and-drop row reordering
- Tracker tags column
Branch: phase-3-ux-updates (PR #10–11)
- Dismissed Jobs tab (view, restore, or save from dismissed list)
- Tracker sort order fix (oldest-first)
- Salary display hardening (guard against crawler sentinel values)
- Saved page unsave/dismiss immediately removes card
- 404 fallback route
Issue: #12
- Responsive / mobile-friendly layout
- Light mode toggle
- Accessibility audit (ARIA, keyboard nav, focus rings)
- Skeleton loading states
- Empty state illustrations
| Sub-phase | Board | Issue |
|---|---|---|
| 4a | Glassdoor | #13 |
| 4b | Wellfound (AngelList) | #14 |
| 4c | ZipRecruiter | #15 |
| 4d | Remote.co + We Work Remotely | #16 |
Issue: #17
- Resume upload + keyword matching score per job
- LLM re-ranking (Claude API) against full job descriptions
- AI cover letter drafts per job card
- Plain-English "Why this job?" fit explanation