Forked and redesigned from JobSearchUK (personal CLI tool) into a full-stack web platform.
A web platform for international students searching for graduate jobs in the UK, featuring automatic visa sponsorship detection and LinkedIn data cached every 6 hours.
Check out the live project here: GradJobsUK - Job Search for International Students
Cron Worker (every 6h)
└─ LinkedIn Guest API → PostgreSQL
↓
FastAPI backend (pure DB queries)
↓
React frontend (Vercel)
Users see only cached data — no real-time LinkedIn requests, so no ban risk.
sequenceDiagram
participant Cron as Cron Worker (6h)
participant Scraper as Scraper.py
participant VC as Visa Verification (3-Layer)
participant DB as PostgreSQL (Supabase)
actor User
participant FE as React Frontend (Vercel)
participant BE as FastAPI Backend (Railway/Render)
Note over Cron, DB: Data Sourcing Phase (Every 6 Hours)
Cron->>Scraper: Trigger periodic scrape
Scraper->>Scraper: Fetch LinkedIn Guest API
Scraper->>VC: Run 3-Layer Verification
Note right of VC: 1. Regex 2. Sponsor Register 3. Verdict
VC-->>Scraper: ✅/⚠️/🟡/❌/Not specified
Scraper->>DB: Upsert job data (Cached)
Note over User, BE: User Interaction Phase
User->>FE: Access Dashboard
FE->>BE: GET /api/jobs (Filter/Sort/Page)
BE->>DB: Pure DB Query (No real-time API)
DB-->>BE: Cached Job Data
BE-->>FE: JSON Results
User->>FE: Upload CV for matching
FE->>BE: POST /api/match (TF-IDF Match)
BE-->>FE: Match Scores & Recommendations
User->>FE: Export Data
FE->>BE: GET /api/export
BE-->>FE: XLSX File
graph TB
subgraph Client_Layer["Frontend - React / Vercel"]
UI["React App"]
Dashboard["Stats Charts & Job Table"]
CV_Match["CV Upload / TF-IDF Match"]
I18n["i18next (EN/ZH/FR/ES/NL)"]
end
subgraph Backend_Layer["Backend - FastAPI / Railway / Render"]
API["FastAPI Routes"]
subgraph Worker_System["Cron Worker - APScheduler"]
Cron["Every 6h Job"]
Scraper["LinkedIn Guest API Scraper"]
end
subgraph Logic_Engine["Processing Engine"]
Parser["HTML Parser"]
VisaEngine["Visa 3-Layer Verification"]
SponsorLoader["GOV.UK CSV Loader"]
end
subgraph Services["API Handlers"]
StatsSvc["Stats Calculator"]
ExportSvc["XLSX Exporter"]
MatchSvc["CV Similarity Scorer"]
end
end
subgraph Storage_Layer["Data Persistence - Supabase / PostgreSQL"]
DB[("PostgreSQL")]
SponsorCSV["backend/data/sponsor_register.csv"]
end
%% Workflow Connections
UI <--> API
Cron --> Scraper
Scraper --> Parser
Parser --> VisaEngine
%% Visa Verification Flow
VisaEngine -->|Layer 1| Regex["Sentence-level Regex"]
VisaEngine -->|Layer 2| FuzzyMatch["GOV.UK Fuzzy Match"]
VisaEngine -->|Layer 3| Verdict["Combined Verdict"]
%% Data Flow
Verdict --> DB
SponsorCSV --> SponsorLoader
SponsorLoader --> FuzzyMatch
API --> DB
%% Legacy Reference
Archive[/"_archive/ Original CLI Tool"/] -.->|Redesigned from| Backend_Layer
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # uses SQLite by default
# Start the API server
uvicorn main:app --reload --port 8000
# Trigger an immediate scrape (in another terminal)
curl -X POST http://localhost:8000/api/admin/scrapecd frontend
npm install
npm run dev
# Open http://localhost:5173docker-compose up
# Backend: http://localhost:8000
# Frontend: http://localhost:5173- Sentence-level regex — positive/negative signals in job descriptions
- GOV.UK Sponsor Register — fuzzy match against official licensed sponsors list
- Combined verdict — 5 possible outcomes (✅
⚠️ 🟡 ❌ Not specified)
To enable Layer 2, download the CSV from GOV.UK and place it at backend/data/sponsor_register.csv.
| Method | Path | Description |
|---|---|---|
| GET | /api/jobs |
Jobs with filtering, pagination, sorting |
| GET | /api/stats |
Dashboard statistics |
| GET | /api/filters |
Available filter values |
| GET | /api/export |
Export filtered jobs as XLSX |
| POST | /api/match |
Upload CV to get TF-IDF match scores |
| POST | /api/admin/scrape |
Manually trigger a scrape |
| Service | Platform | Notes |
|---|---|---|
| Frontend | Vercel | Free tier |
| Backend | Railway / Render | Set DATABASE_URL env var |
| Database | Supabase PostgreSQL | Free tier 500MB |
├── _archive/ # Original CLI tool (preserved at git tag v2-cli-original)
├── backend/
│ ├── main.py # FastAPI entry + APScheduler
│ ├── database.py # SQLAlchemy async models
│ ├── config.py # Constants and keywords
│ ├── scraper/
│ │ ├── worker.py # Cron job main logic
│ │ ├── parser.py # HTML parsing
│ │ ├── visa_checker.py # 3-layer visa verification
│ │ └── sponsor_list.py # GOV.UK CSV loader
│ ├── routers/ # API route handlers (jobs/stats/filters/export/match)
│ ├── data/ # Place sponsor_register.csv here
│ └── requirements.txt
├── frontend/
│ ├── src/
│ │ ├── App.jsx
│ │ ├── components/ # Header, SearchBar, DateTabs, SortTabs,
│ │ │ # FilterSidebar, StatsCards, ChartsPanel,
│ │ │ # JobTable, CVUpload
│ │ ├── hooks/ # useJobs, useStats
│ │ └── i18n/ # en, zh, fr, es, nl
│ └── package.json
└── docker-compose.yml
The original command-line scraper is preserved in _archive/. You can still run it independently:
cd _archive
pip install -r requirements.txt
python scraper.pyThe original version is also tagged in git: v2-cli-original