Abdalla Bakr
All projects

jobhunt

A local-first job board I built because the early-career listings I actually wanted were scattered across a dozen sites and buried under senior roles. It pulls software and entry-level jobs from a couple of hundred boards and APIs into one searchable SQLite database that runs entirely on my own machine, no account and no server.

Python FastAPI SQLite HTMX Jinja Playwright

What this is

A job aggregator that runs on my laptop. It scrapes early-career software roles and zero-experience local work from around 250 boards and APIs (Reed and Jooble via their APIs, plus company boards on Greenhouse, Ashby, Lever, Workday and SmartRecruiters), normalises everything into one SQLite database, and serves it as a fast local web app. It holds tens of thousands of live listings. There’s no hosted version on purpose: it’s a single-user tool, so the whole thing is a pip install and a desktop shortcut, and it works offline once scraped.

How I built it

The stack is deliberately boring where boring is correct: FastAPI + Jinja + HTMX, SQLite with FTS5 for full-text search, no front-end build step. The interesting part is the scrape pipeline. Sources rot constantly, so each one runs through a health check that records what it returned, auto-disables a board that goes silent, and re-probes it later instead of permanently dropping it. Fetches run concurrently behind a semaphore so a full refresh is quick, but each source’s own pages stay sequential so I’m never hammering one host. Listings are de-duplicated across sources by a content key, because the same role shows up on three boards under slightly different titles.

On top of that sits the part that makes it usable day to day: 15 stackable filters over FTS, employment-type normalisation (the raw data had 30+ unnormalised type strings, which I collapsed to four), a fixed UK-city dropdown, and an optional CV-match score that uses a local embedding model when it’s installed and falls back to keyword scoring when it isn’t, so the upload never breaks on a missing dependency.

Decisions worth flagging

  • Local-first over hosted. A hosted version would have meant accounts, a database bill, and scraping other people’s sites from a shared IP. Keeping it on-device sidestepped all three and made “your data never leaves your machine” true by construction.
  • Don’t optimise the scraper without evidence. The concurrent-but-adaptive pipeline has run thousands of fetches with zero rate-limit hits. It would have been easy to “speed it up” and start getting blocked; I left it alone because the data said it was already right.
  • Fixed dropdowns, not database facets. An early version built filter options from whatever values were in the DB, which surfaced dozens of unnormalised employment types. Fixed, curated dropdowns are less clever and far more usable.

Testing

230 unit tests and 22 Playwright end-to-end tests, including regression tests for the filter query builder, the de-duplication key, and the source auto-disable/re-probe logic. The E2E suite drives the real UI in a headless browser so a CSS or template change can’t silently break a filter, which is exactly the kind of bug a unit test would have shrugged at.

Stack

Python, FastAPI, Jinja, HTMX, SQLite (FTS5), Playwright, pytest. Optional sentence-transformers for CV matching.