Skip to content

Tech Stack

Every dependency was researched and validated. Here's what we use and why.

Core

Package Purpose What
Python 3.14+ Language Async I/O everywhere (API, DB, scraping) + the richest AI/data library ecosystem
FastAPI HTTP framework Need auto-generated OpenAPI docs and native async — both missing in Flask/Django
uvicorn ASGI server FastAPI speaks ASGI, not WSGI — uvicorn is the standard ASGI server
Pydantic Data validation & serialization Python doesn't enforce types at runtime — Pydantic validates data boundaries and structures LLM output
SQLAlchemy 2.x ORM + async Handwritten SQL across many tables becomes unmaintainable — ORM maps objects to rows with async support
asyncpg PostgreSQL async driver A synchronous DB driver (psycopg2) blocks the entire async event loop
Alembic DB migrations Schema changes applied manually drift between environments — Alembic tracks and replays them
uv Package manager Reproducible installs need a lockfile; pip doesn't have one, poetry is slow — uv does both fast

Job Data Sources

Source Method Purpose
Adzuna API HTTP API (free) Primary job data source — structured JSON, no scraping
Seek.com.au httpx + beautifulsoup4 Fallback scraper for AU jobs
LinkedIn httpx + beautifulsoup4 Fallback scraper for public listings
~~Indeed~~ ~~playwright~~ Deferred — Cloudflare protection, high ban risk

Networking & Scraping

Package Purpose What
httpx Async HTTP client Need non-blocking HTTP calls throughout the app — httpx is async-native unlike requests
beautifulsoup4 HTML parsing Seek/LinkedIn serve HTML, not JSON — need a parser to extract job data from DOM

LLM Integration

Package Purpose What
litellm Unified LLM client Each LLM provider has its own SDK and API shape — litellm abstracts them into one completion() call

Why LiteLLM?

  • Multi-provider: Claude, GPT, Gemini, Mistral, DeepSeek — switch models by changing a string
  • Unified completion() / acompletion() API — no per-provider code paths
  • Structured output via response_format with Pydantic models
  • Streaming, tool use, fallback/retry all built-in
  • Lightweight wrapper, not a framework

Alternatives considered

Alternative Verdict
anthropic SDK directly Good for Claude-only, but we need multi-provider
pydantic-ai Promising, but LiteLLM is more mature and has wider model coverage
langchain Overkill — too much abstraction for our use case

CLI Client

Package Purpose What
typer Argument parsing CLI needs subcommands, help text, and shell completions — typer generates all from type hints
httpx HTTP client (reused) CLI calls the API server over HTTP — reuses the same httpx already in the project
rich Terminal formatting Job listings and config output need structured display — rich renders tables, progress bars, colors

Web Frontend

The frontend is in frontend/ — see Web App reference for usage.

Package Purpose What
React 19 Web framework Daily job tracking needs a visual dashboard — CLI alone isn't enough for reviewing and managing applications
Vite+ Unified toolchain Wraps Vite 8 (build/dev) + Vitest 4 (tests) + Oxc (lint/fmt) under one vp CLI — one binary instead of stitching together npm scripts for vite/vitest/eslint/prettier
TypeScript 5.7+ Type checking Catches contract drift between frontend and backend Pydantic models at build time
@testing-library/react 16 Component testing Behavioral assertions (queries by role/label) instead of testing implementation details

No UI component library — the design system lives in frontend/src/styles/tokens.css (CSS custom properties) and frontend/src/components/Primitives.tsx (Button, Card, Badge, Chip, etc.). Inline styles bind to the tokens.

Why Vite+ instead of standalone Vite/Vitest?

Vite+ is from VoidZero — the same team behind Vite, Vitest, and Oxc. It's a thin unification layer (still pre-1.0 at v0.1.18 but actively shipping):

  • One CLI for the whole loopvp dev, vp build, vp test, vp check, vp fmt instead of npm run dev / vitest / eslint / prettier
  • Strict type-aware lint included — caught 8 hygiene issues on the first run that vanilla tsc --noEmit had been silent about
  • Bundled Node manager (vp env) — replaces nvm/asdf for this project
  • vp migrate auto-converted the existing Vite/Vitest setup; rollback is just one commit revert if needed

Infrastructure

Tool Purpose What
Docker Compose Local orchestration Local dev needs Postgres running — one docker compose up instead of installing natively
PostgreSQL 18 Database Job data has nested fields (skills, keywords) and needs full-text search — Postgres handles both with JSONB and tsvector
Supabase Cloud database (optional) Some environments can't run Docker — Supabase provides free hosted Postgres with zero setup

Key Decisions

PostgreSQL vs SQLite

  • JSONB native support for skills, keywords, analysis results
  • Full-text search built-in (useful for job searching/filtering)
  • Async driver (asyncpg) pairs perfectly with FastAPI
  • Docker makes local setup trivial (docker compose up -d)
  • Supabase option for cloud hosting (free, no Docker needed)
  • Production-ready if deployment needs change later

Adzuna API vs direct scraping

  • Structured JSON — no HTML parsing, no selector maintenance
  • Free tier (250 requests/day) is sufficient for personal job search
  • Covers AU, US, UK, CA, NZ markets
  • No ban risk — official API with proper credentials
  • Seek/LinkedIn scrapers kept as fallback for coverage gaps

LiteLLM vs direct provider SDKs

  • Multi-provider support is a requirement — need Claude, GPT, Gemini at minimum
  • LiteLLM provides a single interface, avoiding per-provider code paths
  • Fallback chains: if Claude is down, automatically try GPT
  • Cost tracking built-in — useful for monitoring per-job analysis costs
  • Thin wrapper — doesn't hide the underlying API semantics