Case study · personal product

JobTrack

In development

An AI-assisted job-search workspace — import a role, tailor your CV, and track every application from applied to offer.

ASP.NET CoreReactEF CoreFastAPIGmail OAuthDocker
jobtrack.local
JobTrack job workspace: one application with its details, CV match and recruiter thread

TL;DR

What
Full-stack job-search workspace: pipeline, CV match, Gmail threads, attachments, analytics.
Why
A real problem — my own job search needed production-grade tooling, not a spreadsheet.
Stack
React + ASP.NET Core (EF Core) · FastAPI/Ollama local AI · Gmail OAuth · Docker.
Role
Everything: product, backend, frontend, ops and security.

Problem & context

A serious job search spreads across spreadsheets, email threads, notes apps and scattered documents. Nothing shows you, at a glance, which applications need attention or what was said last. I wanted one focused workspace — from importing a role to the final offer — built to the standard I would ship at work, not as a throwaway.

Architecture

A React + TypeScript SPA talks to an ASP.NET Core API that owns the domain logic — pipeline, follow-up rules, CV keyword matching — persisting via EF Core with attachments on disk. A small FastAPI service backed by a local Ollama model drafts CVs, cover letters and follow-ups, and the API imports correspondence from the Gmail API over OAuth2. The whole thing runs behind one Docker Compose file.

  • Pipeline: a Kanban board — Applied, Waiting, Interview, Offer, Rejected, Ghosted — drag to update.
  • CV match: deterministic keyword coverage (matched vs missing), not a black-box score.
  • Gmail: import full threads over OAuth2; linked threads auto-refresh onto the right job.
  • AI is assistive, never autonomous: it drafts, you always review and send — no auto-apply.
JobTrack architecture A React single-page app talks to an ASP.NET Core API, which persists via EF Core with attachments on disk, calls a FastAPI/Ollama AI service, and imports from the external Gmail API over OAuth2. /api import React SPA nginx · PWA ASP.NET Core API auth · rules · CV match EF Core + files data · attachments AI service FastAPI → Ollama Gmail API external · OAuth2

Key decisions & trade-offs

  1. 01 Run the AI locally with Ollama instead of a cloud API.

    alt: A hosted LLM API would have been faster to wire up.

    Job-search data is sensitive and I wanted zero per-call cost and no third party in the loop. The trade-off is more setup and heavier local resources — acceptable for a self-hosted tool.

  2. 02 Make CV matching deterministic, not an AI score.

    alt: Let the model rate the fit.

    A number a candidate can’t interrogate is useless. Deterministic keyword coverage shows exactly which terms matched and which are missing, so the advice is honest and actionable.

  3. 03 Ship the PWA with no offline service-worker cache.

    alt: A cache would enable full offline use.

    I deploy frequently, so an aggressive cache risks serving stale builds — a worse failure than a brief offline gap. The manifest still provides installability and share-to-capture. A deliberate anti-feature.

Security & production notes

  • Optional Google sign-in (Google ID tokens) protects the API; every record is scoped to its owner.
  • File uploads are validated and stored per-application with ownership checks on every access.
  • The AI layer is advisory only — it drafts, it never sends or auto-applies.
  • Runs as a reproducible Docker Compose stack with a documented .env; JSON/CSV exports for data portability.

Screenshots

JobTrack Kanban pipeline with applications across Applied, Waiting, Interview and Offer
Pipeline — drag applications between stages, from Applied to Offer
JobTrack dashboard with response rate, funnel and time-in-stage
Dashboard — response rates, funnel and skill demand across the search

Status & what’s next

In active development. The follow-up rules are simple date logic I’d like to make configurable per stage; next up is a proper integration-test pass around the Gmail import edge cases and tighter grounding on the AI drafts now that I have real usage to learn from.